diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2de55fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +# Byte-compiled / optimized / DLL files +.idea/ +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +.vscode +mydoc \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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/Pipfile b/Pipfile new file mode 100644 index 0000000..d815e4b --- /dev/null +++ b/Pipfile @@ -0,0 +1,33 @@ +[[source]] +name = "pypi" +url = "https://mirrors.163.com/pypi/simple/" +verify_ssl = true + +[dev-packages] +#mypy = "*" +#fastapi = "*" +#uvicorn = "*" +#jinja2 = "*" +pytest = "*" + +[packages] +aiohttp = "*" +lxml = "*" +#bitarray = "*" +requests = "*" +fastapi = "*" +uvicorn = {extras = ["standard"],version = "*"} +python-multipart = "*" +ruia = "*" +ruia-ua = "*" +jsonpath = "*" +parsel = "*" +pytest = "*" +pyppeteer = "*" +pymysql = "*" +aiomysql = "*" +mkdocs = "*" +cchardet = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..6efad0e --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,278 @@ +{ + "_meta": { + "hash": { + "sha256": "5154b1655506994ecfe02d649e521625a8e07831b60a5bfcba75e82c54d25212" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://mirrors.163.com/pypi/simple/", + "verify_ssl": true + } + ] + }, + "default": { + "aiohttp": { + "hashes": [ + "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", + "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", + "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", + "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", + "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", + "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", + "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", + "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", + "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", + "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", + "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", + "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" + ], + "index": "pypi", + "version": "==3.6.2" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "certifi": { + "hashes": [ + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + ], + "version": "==2020.12.5" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + ], + "version": "==2.9" + }, + "lxml": { + "hashes": [ + "sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f", + "sha256:08fc93257dcfe9542c0a6883a25ba4971d78297f63d7a5a26ffa34861ca78730", + "sha256:107781b213cf7201ec3806555657ccda67b1fccc4261fb889ef7fc56976db81f", + "sha256:121b665b04083a1e85ff1f5243d4a93aa1aaba281bc12ea334d5a187278ceaf1", + "sha256:1fa21263c3aba2b76fd7c45713d4428dbcc7644d73dcf0650e9d344e433741b3", + "sha256:2b30aa2bcff8e958cd85d907d5109820b01ac511eae5b460803430a7404e34d7", + "sha256:4b4a111bcf4b9c948e020fd207f915c24a6de3f1adc7682a2d92660eb4e84f1a", + "sha256:5591c4164755778e29e69b86e425880f852464a21c7bb53c7ea453bbe2633bbe", + "sha256:59daa84aef650b11bccd18f99f64bfe44b9f14a08a28259959d33676554065a1", + "sha256:5a9c8d11aa2c8f8b6043d845927a51eb9102eb558e3f936df494e96393f5fd3e", + "sha256:5dd20538a60c4cc9a077d3b715bb42307239fcd25ef1ca7286775f95e9e9a46d", + "sha256:74f48ec98430e06c1fa8949b49ebdd8d27ceb9df8d3d1c92e1fdc2773f003f20", + "sha256:786aad2aa20de3dbff21aab86b2fb6a7be68064cbbc0219bde414d3a30aa47ae", + "sha256:7ad7906e098ccd30d8f7068030a0b16668ab8aa5cda6fcd5146d8d20cbaa71b5", + "sha256:80a38b188d20c0524fe8959c8ce770a8fdf0e617c6912d23fc97c68301bb9aba", + "sha256:8f0ec6b9b3832e0bd1d57af41f9238ea7709bbd7271f639024f2fc9d3bb01293", + "sha256:92282c83547a9add85ad658143c76a64a8d339028926d7dc1998ca029c88ea6a", + "sha256:94150231f1e90c9595ccc80d7d2006c61f90a5995db82bccbca7944fd457f0f6", + "sha256:9dc9006dcc47e00a8a6a029eb035c8f696ad38e40a27d073a003d7d1443f5d88", + "sha256:a76979f728dd845655026ab991df25d26379a1a8fc1e9e68e25c7eda43004bed", + "sha256:aa8eba3db3d8761db161003e2d0586608092e217151d7458206e243be5a43843", + "sha256:bea760a63ce9bba566c23f726d72b3c0250e2fa2569909e2d83cda1534c79443", + "sha256:c3f511a3c58676147c277eff0224c061dd5a6a8e1373572ac817ac6324f1b1e0", + "sha256:c9d317efde4bafbc1561509bfa8a23c5cab66c44d49ab5b63ff690f5159b2304", + "sha256:cc411ad324a4486b142c41d9b2b6a722c534096963688d879ea6fa8a35028258", + "sha256:cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6", + "sha256:cfd7c5dd3c35c19cec59c63df9571c67c6d6e5c92e0fe63517920e97f61106d1", + "sha256:e1cacf4796b20865789083252186ce9dc6cc59eca0c2e79cca332bdff24ac481", + "sha256:e70d4e467e243455492f5de463b72151cc400710ac03a0678206a5f27e79ddef", + "sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd", + "sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee" + ], + "index": "pypi", + "version": "==4.5.2" + }, + "multidict": { + "hashes": [ + "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", + "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", + "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", + "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", + "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", + "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", + "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", + "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", + "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", + "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", + "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", + "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", + "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", + "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", + "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", + "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", + "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" + ], + "version": "==4.7.6" + }, + "requests": { + "hashes": [ + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + ], + "index": "pypi", + "version": "==2.23.0" + }, + "urllib3": { + "hashes": [ + "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", + "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + ], + "version": "==1.25.9" + }, + "yarl": { + "hashes": [ + "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", + "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", + "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", + "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", + "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", + "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", + "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", + "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", + "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", + "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", + "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", + "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", + "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", + "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", + "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", + "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", + "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" + ], + "version": "==1.4.2" + } + }, + "develop": { + "jinja2": { + "hashes": [ + "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", + "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" + ], + "index": "pypi", + "version": "==2.11.2" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" + ], + "version": "==1.1.1" + }, + "mypy": { + "hashes": [ + "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324", + "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc", + "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802", + "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122", + "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975", + "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7", + "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666", + "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669", + "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178", + "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01", + "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea", + "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de", + "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1", + "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c" + ], + "index": "pypi", + "version": "==0.790" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "typed-ast": { + "hashes": [ + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + ], + "version": "==1.4.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", + "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", + "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" + ], + "version": "==3.7.4.2" + } + } +} diff --git a/launcher.py b/launcher.py new file mode 100644 index 0000000..db134b3 --- /dev/null +++ b/launcher.py @@ -0,0 +1,65 @@ +import asyncio +import atexit +import threading +import time +from multiprocessing.pool import Pool + +from smart.log import log +from smart.pipline import Piplines +from smart.runer import CrawStater +from spiders.db.sanicdb import SanicDB +from spiders.govs import GovsSpider, ArticelItem +from spiders.ipspider2 import IpSpider3, GovSpider, IpSpider, ApiSpider +from spiders.js.js_spider import JsSpider, Broswer +from spiders.json_spider import JsonSpider +from test import middleware2 + +piplinestest = Piplines() + + +@piplinestest.pipline(1) +async def do_pip(spider_ins, item): + return item + + +@piplinestest.pipline(2) +def do_pip2(spider_ins, item): + print(f"我是item2 {item.results}") + return item + + +db = SanicDB('localhost', 'testdb', 'root', 'root', + minsize=5, maxsize=55, + connect_timeout=10 + ) + + +@atexit.register +def when_end(): + global db + if db: + db.close() + + +@piplinestest.pipline(3) +async def to_mysql_db(spider_ins, item): + if item and isinstance(item, ArticelItem): + print(f"我是item3 入库 {item.results}") + global db + last_id = await db.table_insert("art", item.results) + print(f"last_id {last_id}") + + return item + + +def start1(): + starter = CrawStater() + starter.run_single(IpSpider(), middlewire=middleware2, pipline=piplinestest) + + +if __name__ == '__main__': + starter = CrawStater() + spider1 = GovsSpider() + spider2 = JsonSpider() + js_spider = JsSpider() + starter.run_many([IpSpider()], middlewire=middleware2, pipline=piplinestest) diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..7232ca0 --- /dev/null +++ b/readme.md @@ -0,0 +1,7 @@ +## Smart-spider +Smart-spider + + + + + diff --git a/smart/__init__.py b/smart/__init__.py new file mode 100644 index 0000000..eae9ee8 --- /dev/null +++ b/smart/__init__.py @@ -0,0 +1,7 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: __init__.py +# Author: liangbaikai +# Date: 2020/12/21 +# Desc: there is smart-framework core package +# ------------------------------------------------------------------ \ No newline at end of file diff --git a/smart/core.py b/smart/core.py new file mode 100644 index 0000000..1cf5b73 --- /dev/null +++ b/smart/core.py @@ -0,0 +1,229 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: core +# Author: liangbaikai +# Date: 2020/12/22 +# Desc: there is a python file description +# ------------------------------------------------------------------ +import asyncio +import importlib +import inspect +import time +import traceback +import uuid +from asyncio import Lock +from collections import deque +from contextlib import suppress +from typing import Dict + +from smart.log import log +from smart.downloader import Downloader +from smart.item import Item +from smart.pipline import Piplines +from smart.request import Request +from smart.scheduler import Scheduler +from smart.setting import gloable_setting_dict + + +class Engine: + def __init__(self, spider, middlewire=None, pipline: Piplines = None): + self.lock = None + self.task_dict: Dict[str, asyncio.Task] = {} + self.pip_task_dict: Dict[str, asyncio.Task] = {} + self.spider = spider + self.middlewire = middlewire + self.piplines = pipline + duplicate_filter_class = self._get_dynamic_class_setting("duplicate_filter_class") + scheduler_container_class = self._get_dynamic_class_setting("scheduler_container_class") + net_download_class = self._get_dynamic_class_setting("net_download_class") + self.scheduler = Scheduler(duplicate_filter_class(), scheduler_container_class()) + req_per_concurrent = self.spider.cutome_setting_dict.get("req_per_concurrent") or gloable_setting_dict.get( + "req_per_concurrent") + self.downloader = Downloader(self.scheduler, self.middlewire, seq=req_per_concurrent, + downer=net_download_class()) + self.request_generator_queue = deque() + self.stop = False + self.log = log + + def _get_dynamic_class_setting(self, key): + class_str = self.spider.cutome_setting_dict.get( + key) or gloable_setting_dict.get( + key) + _module = importlib.import_module(".".join(class_str.split(".")[:-1])) + _class = getattr(_module, class_str.split(".")[-1]) + return _class + + def iter_request(self): + while True: + if not self.request_generator_queue: + yield None + continue + request_generator = self.request_generator_queue[0] + spider, real_request_generator = request_generator[0], request_generator[1] + try: + # execute and get a request from cutomer code + # request=real_request_generator.send(None) + request_or_item = next(real_request_generator) + if isinstance(request_or_item, Request): + request_or_item.__spider__ = spider + except StopIteration: + self.request_generator_queue.popleft() + continue + except Exception as e: + # 可以处理异常 + self.request_generator_queue.popleft() + self._handle_exception(spider, e) + continue + yield request_or_item + + def _check_complete_pip(self, task): + if task.cancelled(): + self.log.debug(f" a task canceld ") + return + if task and task.done() and task._key: + if task.exception(): + self.log.error(f"a task occurer error in pipline {task.exception()} ") + else: + self.log.debug(f"a task done ") + result = task.result() + if result and isinstance(result, Item): + if hasattr(task, '_index'): + self._hand_piplines(task._spider, result, task._index + 1) + self.pip_task_dict.pop(task._key) + + def _check_complete_callback(self, task): + if task.cancelled(): + self.log.debug(f" a task canceld ") + return + if task and task.done() and task._key: + self.log.debug(f"a task done ") + self.task_dict.pop(task._key) + + async def start(self): + self.spider.on_start() + # self.spider + self.request_generator_queue.append((self.spider, iter(self.spider))) + # self.request_generator_queue.append( iter(self.spider)) + # core implenment + while not self.stop: + # paused + if self.lock and self.lock.locked(): + await asyncio.sleep(1) + continue + + request_or_item = next(self.iter_request()) + if isinstance(request_or_item, Request): + self.scheduler.schedlue(request_or_item) + + if isinstance(request_or_item, Item): + self._hand_piplines(self.spider, request_or_item) + + request = self.scheduler.get() + can_stop = self._check_can_stop(request) + # if request is None and not self.task_dict: + if can_stop: + # there is no request and the task has been completed.so ended + self.log.debug( + f" here is no request and the task has been completed.so engine will stop ..") + self.stop = True + break + if isinstance(request, Request): + self._ensure_future(request) + + resp = self.downloader.get() + + if resp is None: + # let the_downloader can be scheduled, test 0.001-0.0006 is better + await asyncio.sleep(0.0005) + continue + + custome_callback = resp.request.callback + if custome_callback: + request_generator = custome_callback(resp) + if request_generator: + self.request_generator_queue.append((custome_callback.__self__, request_generator)) + # self.request_generator_queue.append( request_generator) + if self.spider.state != "runing": + self.spider.state = "runing" + + self.spider.state = "closed" + self.spider.on_close() + self.log.debug(f" engine stoped..") + await asyncio.sleep(0.15) + + def pause(self): + self.log.info(f" out called pause.. so engine will pause.. ") + asyncio.create_task(self._lock()) + self.spider.state = "pause" + + def recover(self): + if self.lock and self.lock.locked(): + self.log.info(f" out called recover.. so engine will recover.. ") + self.lock.release() + + def close(self): + # can make external active end engine + self.stop = True + tasks = asyncio.all_tasks() + for it in tasks: + it.cancel() + asyncio.gather(*tasks, return_exceptions=True) + self.log.debug(f" out called stop.. so engine close.. ") + + async def _lock(self): + if self.lock is None: + self.lock = Lock() + await self.lock.acquire() + + def _ensure_future(self, request: Request): + # compatible py_3.6 + task = asyncio.ensure_future(self.downloader.download(request)) + key = str(uuid.uuid4()) + task._key = key + self.task_dict[key] = task + task.add_done_callback(self._check_complete_callback) + + def _handle_exception(self, spider, e): + if spider: + try: + self.log.error(f" occured exceptyion e {e} ", exc_info=True) + spider.on_exception_occured(e) + except BaseException: + pass + + def _check_can_stop(self, request): + if request: + return False + if len(self.task_dict) > 0: + return False + if len(self.request_generator_queue) > 0: + return False + if self.downloader.response_queue.qsize() > 0: + return False + if len(self.pip_task_dict) > 0: + return False + return True + + def _hand_piplines(self, spider_ins, item, index=0): + if self.piplines is None or len(self.piplines.piplines) <= 0: + self.log.info("get a item but can not find a piplinse to handle it so ignore it ") + return + + if len(self.piplines.piplines) < index + 1: + return + + pip = self.piplines.piplines[index][1] + + if not callable(pip): + return + + if not inspect.iscoroutinefunction(pip): + task = asyncio.get_running_loop().run_in_executor(None, pip, spider_ins, item) + else: + task = asyncio.ensure_future(pip(spider_ins, item)) + key = str(uuid.uuid4()) + task._key = key + task._index = index + task._spider = spider_ins + self.pip_task_dict[key] = task + task.add_done_callback(self._check_complete_pip) diff --git a/smart/downloader.py b/smart/downloader.py new file mode 100644 index 0000000..4be863f --- /dev/null +++ b/smart/downloader.py @@ -0,0 +1,189 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: downloader +# Author: liangbaikai +# Date: 2020/12/21 +# Desc: there is a python file description +# ------------------------------------------------------------------ +import asyncio +import inspect +from abc import ABC, abstractmethod +from asyncio import Queue, QueueEmpty +from contextlib import suppress +from typing import Optional +import aiohttp +from concurrent.futures import TimeoutError + +from smart.log import log +from smart.middlewire import Middleware +from smart.response import Response +from smart.scheduler import Scheduler +from smart.setting import gloable_setting_dict +from .request import Request + + +class BaseDown(ABC): + + @abstractmethod + def fetch(self, request: Request) -> Response: + pass + + +# class RequestsDown(BaseDown): +# def fetch(self, request: Request) -> Response: +# import requests +# res = requests.get(request.url, +# timeout=request.timeout or 3, +# ) +# response = Response(body=res.content, request=request, +# headers=res.headers, +# cookies=res.cookies, +# status=res.status_code) +# return response + + +class AioHttpDown(BaseDown): + + async def fetch(self, request: Request) -> Response: + async with aiohttp.ClientSession() as clicnt: + resp = await clicnt.request(request.method, + request.url, + timeout=request.timeout or 10, + headers=request.header or {}, + cookies=request.cookies or {}, + data=request.data or {}, + **request.extras or {} + ) + byte_content = await resp.read() + headers = {} + if resp.headers: + headers = {k: v for k, v in resp.headers.items()} + response = Response(body=byte_content, + status=resp.status, + headers=headers, + cookies=resp.cookies + ) + return response + + +class Downloader: + + def __init__(self, scheduler: Scheduler, middwire: Middleware = None, seq=100, downer: BaseDown = AioHttpDown()): + self.log = log + self.scheduler = scheduler + self.middwire = middwire + self.response_queue: asyncio.Queue = Queue() + # the file handle opens too_much to report an error + self.semaphore = asyncio.Semaphore(seq) + # the real to fetch resource from internet + self.downer = downer + self.log.info(f" downer loaded {self.downer.__class__.__name__}") + async def download(self, request: Request): + spider = request.__spider__ + max_retry = spider.cutome_setting_dict.get("req_max_retry") or gloable_setting_dict.get( + "req_max_retry") + if max_retry <= 0: + raise ValueError("req_max_retry must >0") + header_dict = spider.cutome_setting_dict.get("default_headers") or gloable_setting_dict.get( + "default_headers") + req_timeout = request.timeout or spider.cutome_setting_dict.get("req_timeout") or gloable_setting_dict.get( + "req_timeout") + request.timeout = req_timeout + header = request.header or {} + request.header = header.update(header_dict) + request.header = header + ignore_response_codes = spider.cutome_setting_dict.get("ignore_response_codes") or gloable_setting_dict.get( + "ignore_response_codes") + req_delay = spider.cutome_setting_dict.get("req_delay") or gloable_setting_dict.get("req_delay") + + if request and request.retry >= max_retry: + # reached max retry times + self.log.error(f'reached max retry times... {request}') + return + request.retry = request.retry + 1 + # when canceled + loop = asyncio.get_running_loop() + if loop.is_closed() or not loop.is_running(): + self.log.warning(f'loop is closed in download') + return + with suppress(asyncio.CancelledError): + async with self.semaphore: + await self._before_fetch(request) + + fetch = self.downer.fetch + iscoroutinefunction = inspect.iscoroutinefunction(fetch) + # support sync or async request + try: + # req_delay + if req_delay > 0: + await asyncio.sleep(req_delay) + self.log.debug( + f"send a request: \r\n【 \r\n url: {request.url} \r\n method: {request.method} \r\n header: {request.header} \r\n 】") + # + if iscoroutinefunction: + response = await fetch(request) + else: + self.log.debug(f'fetch may be an snyc func so it will run in executor ') + response = await asyncio.get_event_loop() \ + .run_in_executor(None, fetch, request) + except TimeoutError as e: + # delay retry + self.scheduler.schedlue(request) + self.log.debug( + f'req to fetch is timeout now so this req will dely to sechdule for retry {request.url}') + return + except asyncio.CancelledError as e: + self.log.debug(f' task is cancel..') + return + except BaseException as e: + self.log.error(f'occured some exception in downloader e:{e}') + return + if response is None or not isinstance(response, Response): + self.log.error( + f'the downer {self.downer.__class__.__name__} fetch function must return a response,' + 'that is a no-null response, and response must be a ' + 'smart.Response instance or sub Response instance. ') + return + + if response.status not in ignore_response_codes: + await self._after_fetch(request, response) + + if response.status not in ignore_response_codes: + response.request = request + response.__spider__ = spider + await self.response_queue.put(response) + + def get(self) -> Optional[Response]: + with suppress(QueueEmpty): + return self.response_queue.get_nowait() + + async def _before_fetch(self, request): + if self.middwire and len(self.middwire.request_middleware) > 0: + for item_tuple in self.middwire.request_middleware: + user_func = item_tuple[1] + if callable(user_func): + try: + # res not used + if inspect.iscoroutinefunction(user_func): + res = await user_func(request.__spider__, request) + else: + res = await asyncio.get_event_loop() \ + .run_in_executor(None, user_func, request.__spider__, request) + except Exception as e: + self.log.error(f"in middwire,before do send a request occured an error: {e}", exc_info=True) + return + + async def _after_fetch(self, request, response): + if response and self.middwire and len(self.middwire.response_middleware) > 0: + for item_tuple in self.middwire.response_middleware: + if callable(item_tuple[1]): + try: + # res not used + if inspect.iscoroutinefunction(item_tuple[1]): + res = await item_tuple[1](request.__spider__, request, response) + else: + res = await asyncio.get_event_loop() \ + .run_in_executor(None, item_tuple[1], request.__spider__, request, response) + except Exception as e: + self.log.error(f"in middwire,after a request sended, occured an error: {e}", exc_info=True) + return diff --git a/smart/field.py b/smart/field.py new file mode 100644 index 0000000..14e77a7 --- /dev/null +++ b/smart/field.py @@ -0,0 +1,2330 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: item +# Author: liangbaikai +# Date: 2020/12/31 +# Desc: there is a python file description +# ------------------------------------------------------------------ +import json +import re +from abc import abstractmethod, ABC +from typing import Union, Iterable, Callable, Any + +import jsonpath +from lxml import etree +from lxml.etree import _ElementUnicodeResult + + +class BaseField: + + def __init__(self, default=None, many: bool = False): + self.default = default + self.many = many + + def extract(self, *args, **kwargs): + ... + + +class _LxmlElementField(BaseField): + def __init__( + self, + css_select: str = None, + xpath_select: str = None, + default='', + many: bool = False, + ): + """ + :param css_select: css select http://lxml.de/cssselect.html + :param xpath_select: http://www.w3school.com.cn/xpath/index.asp + :param default: inherit + :param many: inherit + """ + super(_LxmlElementField, self).__init__(default=default, many=many) + self.css_select = css_select + self.xpath_select = xpath_select + + def _get_elements(self, *, html_etree: etree._Element): + if self.css_select: + elements = html_etree.cssselect(self.css_select) + elif self.xpath_select: + elements = html_etree.xpath(self.xpath_select) + else: + raise ValueError( + f"{self.__class__.__name__} field: css_select or xpath_select is expected." + ) + if not self.many: + elements = elements[:1] + return elements + + def _parse_element(self, element): + raise NotImplementedError + + def extract(self, html: Union[etree._Element, str]): + if html is None: + raise ValueError("html_etree can not be null..") + + if html and not isinstance(html, etree._Element): + html = etree.HTML(html) + + elements = self._get_elements(html_etree=html) + + # if is_source: + # return elements if self.many else elements[0] + + if elements: + results = [self._parse_element(element) for element in elements] + elif self.default is None: + raise ValueError( + f"Extract `{self.css_select or self.xpath_select}` error, " + "please check selector or set parameter named `default`" + ) + else: + results = self.default if type(self.default) == list else [self.default] + + return results if self.many else results[0] + + +class AttrField(_LxmlElementField): + """ + This field is used to get attribute. + """ + + def __init__( + self, + attr, + css_select: str = None, + xpath_select: str = None, + default="", + many: bool = False, + ): + super(AttrField, self).__init__( + css_select=css_select, xpath_select=xpath_select, default=default, many=many + ) + self.attr = attr + + def _parse_element(self, element): + return element.get(self.attr, self.default) + + +class ElementField(_LxmlElementField): + """ + This field is used to get LXML element(s). + """ + + def _parse_element(self, element): + return element + + +class HtmlField(_LxmlElementField): + """ + This field is used to get raw html data. + """ + + def _parse_element(self, element): + if element is None: + return None + if isinstance(element, _ElementUnicodeResult): + res = element.encode("utf-8").decode(encoding="utf-8") + else: + res = etree.tostring(element, encoding="utf-8").decode(encoding="utf-8") + if res: + res = res.strip() + return res + + +class TextField(_LxmlElementField): + """ + This field is used to get text. + """ + + def _parse_element(self, element): + # Extract text appropriately based on it's type + if isinstance(element, etree._ElementUnicodeResult): + strings = [node for node in element] + else: + strings = [node for node in element.itertext()] + + string = "".join(strings) + return string if string else self.default + + +class JsonPathField(BaseField): + def __init__(self, json_path: str, default="", many: bool = False): + super(JsonPathField, self).__init__(default=default, many=many) + self._json_path = json_path + + def extract(self, html: Union[str, dict, etree._Element]): + if isinstance(html, etree._Element): + html = etree.tostring(html).decode(encoding="utf-8") + if isinstance(html, str) or isinstance(html, etree._Element): + html = json.loads(html) + json_loads = html + res = jsonpath.jsonpath(json_loads, self._json_path) + if isinstance(res, bool) and not res: + return self.default + if self.many: + if isinstance(res, Iterable): + return res + else: + return [res] + else: + if isinstance(res, Iterable) and not isinstance(res, str): + return res[0] + else: + return res + + +class RegexField(BaseField): + """ + This field is used to get raw html code by regular expression. + RegexField uses standard library `re` inner, that is to say it has a better performance than _LxmlElementField. + """ + + def __init__(self, re_select: str, re_flags=0, default="", many: bool = False): + super(RegexField, self).__init__(default=default, many=many) + self._re_select = re_select + self._re_object = re.compile(self._re_select, flags=re_flags) + + def _parse_match(self, match): + if not match: + if self.default is not None: + return self.default + else: + raise ValueError( + f"Extract `{self._re_select}` error, can not founded " + f"please check selector or set parameter named `default`" + ) + else: + string = match.group() + groups = match.groups() + group_dict = match.groupdict() + if group_dict: + return group_dict + if groups: + return groups[0] if len(groups) == 1 else groups + return string + + def extract(self, html: Union[str, dict, etree._Element]): + if isinstance(html, etree._Element): + html = etree.tostring(html).decode(encoding="utf-8") + if isinstance(html, dict): + html = json.dumps(html, ensure_ascii=False) + if self.many: + matches = self._re_object.finditer(html) + return [self._parse_match(match) for match in matches] + else: + match = self._re_object.search(html) + return self._parse_match(match) + + +class FuncField(BaseField): + def __init__(self, call: Callable, name: str, default="", many: bool = False): + super(FuncField, self).__init__(default=default, many=many) + self._callable = call + if not callable(self._callable): + raise TypeError("callable param need a function or cab be called") + self._name = name + + def extract(self, html: Any): + res = self._callable(html, self._name) + if self.many: + if isinstance(res, Iterable): + return res + else: + return [res] + else: + if isinstance(res, Iterable) and not isinstance(res, str): + return res[0] + else: + return res + + +if __name__ == '__main__': + html = """ + + + + + + + +武动乾坤小说_天蚕土豆_武动乾坤最新章节_武动乾坤无弹窗_新笔趣阁 + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + +
+
+
+ 新笔趣阁 > 玄幻小说 > 武动乾坤最新章节目录 +
+
+
+

武动乾坤

+

作    者:天蚕土豆

+

动    作:加入书架, 投推荐票, 直达底部

+

最后更新:2017-11-09 06:33:19

+

最新章节:新书大主宰已发。

+
+
+

手机阅读《武动乾坤》无弹窗纯文字全文免费阅读 + +

+ +

修炼一途,乃窃阴阳,夺造化,转涅盘,握生死,掌轮回。 + 武之极,破苍穹,动乾坤! + 新书求收藏,求推荐,谢大家o(n_n)o~ +

+
+
+ + +
+ +
+ +
+
+
+ + +
第一章 林动
+
第二章 通背拳
+
第三章 古怪的石池
+
第四章 石池之秘
+ + +
第五章 神秘石符
+
第六章 七响
+
第七章 淬体第四重
+
第八章 冲突
+ + +
第九章 林宏
+
第十章 金玉枝
+
第十一章 阴珠
+
第十二章 第十响
+ + +
第十三章 疗伤
+
第十四章 五等阴煞之气
+
第十五章 淬体第五重
+
第十六章 八荒掌
+ + +
第十七章 蝎虎
+
第十八章 元力种子
+
第十九章 族比前的突破
+
第二十章 族比开始
+ + +
第二十一章 林陨
+
第二十二章 艺惊全场
+
第二十三章 前三
+
第二十四章 完胜
+ + +
第二十五章 接管事务
+
第二十六章 狩猎
+
第二十七章 武学馆
+
第二十八章 奇门印,残篇
+ + +
第二十九章 石符变故
+
第三十章 小成
+
第三十一章 妖孽
+
第三十二章 地下交易所
+ + +
第三十三章 谢婷
+
第三十四章 雷力
+
第三十五章 初步交手
+
第三十六章 聚餐
+ + +
第三十七章 突破
+
第三十八章 变故
+
第三十九章 地元境!
+
第四十章 狩猎开始
+ + +
第四十一章 罗城
+
第四十二章 火蟒虎
+
第四十三章 抢崽
+
第四十四章 得手
+ + +
第四十五章 剑拔弩张
+
第四十六章 震惊全场
+
第四十七章 激战
+
第四十八章 收获
+ + +
第四十九章 武学奇才
+
第五十章 青元功
+
第五十一章 小炎
+
第五十二章 家族之事
+ + +
第五十三章 铁木庄
+
第五十四章 毁土
+
第五十五章 搏杀
+
第五十六章 泥土中的阳罡之气
+ + +
第五十七章 阳元石
+
第五十八章 矿脉
+
第五十九章 杀豹
+
第六十章 磨练
+ + +
第六十一章 阳元丹
+
第六十二章 炎城
+
第六十三章 符师
+
第六十四章 岩大师
+ + +
第六十五章 绊子
+
第六十六章 神动篇
+
第六十七章 阴云
+
第六十八章 黑龙寨
+ + +
第六十九章 大难
+
第七十章 震撼
+
第七十一章 突破!
+
第七十二章 退敌
+ + +
第七十三章 暴怒的林震天
+
第七十四章 血洗黑龙寨
+
第七十五章 碎元梭
+
第七十六章 神秘兽骸
+ + +
第七十七章 妖异花朵
+
第七十八章 暴涨的精神力
+
第七十九章 地下坊会
+
第八十章 遇袭
+ + +
第八十一章 反杀
+
第八十二章 一死一伤
+
第八十三章 古木
+
第八十四章 古漩符印
+ + +
第八十五章 一印符师
+
第八十六章 救援
+
第八十七章 断后
+
第八十八章 突破
+ + +
第八十九章 试探
+
第九十章 小元丹境
+
第九十一章 联姻
+
第九十二章 雷谢两家的打算
+ + +
第九十三章 古大师
+
第九十四章 暴露
+
第九十五章 符师对决
+
第九十六章 本命灵符
+ + +
第九十七章 杀!
+
第九十八章 隐患
+
第九十九章 石符内的“鼠”
+
第一百章 天妖貂
+ + +
第一百零一章 血衣临门
+
第一百零二章 赌约
+
第一百零三章 暂离
+
第一百零四章 万金拍卖场
+ + +
第一百零五章 销金窟
+
第一百零六章 萱素
+
第一百零七章 丹仙池
+
第一百零八章 尖螺波
+ + +
第一百零九章 宋青
+
第一百一十章 动身
+
第一百一十一章 仙池之争
+
第一百一十二章 最后一战
+ + +
第一百一十三章 化血归元功
+
第一百一十四章 进入丹仙池
+
第一百一十五章 化气精旋
+
第一百一十六章 碧水妖蟒
+ + +
第一百一十七章 两兽相斗
+
第一百一十八章 供奉与花销
+
第一百一十九章 赚钱
+
第一百二十章 三阳决
+ + +
第一百二十一章 苦修
+
第一百二十二章 福不单行
+
第一百二十三章 小元丹,二印符师
+
第一百二十四章 展现实力
+ + +
第一百二十五章 生死斗
+
第一百二十六章 对战魏通
+
第一百二十七章 激战
+
第一百二十八章 杀!
+ + +
第一百二十九章 落幕
+
第一百三十章 塔斗
+
第一百三十一章 紫月
+
第一百三十二章 再说一次
+ + +
第一百三十三章 曹铸
+
第一百三十四章 冰玄剑
+
第一百三十五章 塔斗开始
+
第一百三十六章 第五层
+ + +
第一百三十七章 打劫
+
第一百三十八章 追赶
+
第一百三十九章 进入第七层
+
第一百四十章 意志
+ + +
第一百四十一章 化生符阵
+
第一百四十二章 胜负
+
第一百四十三章 三印符师
+
第一百四十四章 祖符
+ + +
第一百四十五章 精神地
+
第一百四十六章 一波再起
+
第一百四十七章 鸟东西
+
第一百四十八章 指教
+ + +
第一百四十九章 对战鬼阎
+
第一百五十章 化生符阵显威
+
第一百五十一章 四大势力
+
第一百五十二章 震慑
+ + +
第一百五十三章 煞魔之体
+
第一百五十四章 妖血朱果
+
第一百五十五章 古墓府
+
第一百五十六章 内族之人
+ + +
第一百五十七章 林尘
+
第一百五十八章 完美的操控
+
第一百五十九章 小圆满
+
第一百六十章 天炎山脉
+ + +
第一百六十一章 宋刀
+
第一百六十二章 灵宝
+
第一百六十三章 化生符阵第三重
+
第一百六十四章 强夺
+ + +
第一百六十五章 夜色下的落幕
+
第一百六十六章 林琅天
+
第一百六十七章 四大年轻顶尖强者!
+
第一百六十八章 破封
+ + +
第一百六十九章 暴富
+
第一百七十章 洗劫妖灵室
+
第一百七十一章 六件灵宝
+
第一百七十二章 抢宝
+ + +
第一百七十三章 天鳞古戟
+
第一百七十四章 符傀
+
第一百七十五章 中等符傀
+
第一百七十六章 火海
+ + +
第一百七十七章 涅盘心
+
第一百七十八章 强夺阳气
+
第一百七十九章 墓府主人
+
第一百八十章 麻烦
+ + +
第一百八十一章 今日事,百倍还
+
第一百八十二章 符祖
+
第一百八十三章 激斗王炎!
+
第一百八十四章 符傀之威
+ + +
第一百八十五章 救援
+
第一百八十六章 山顶之谈
+
第一百八十七章 收获
+
第一百八十八章 血拼
+ + +
第一百八十九章 以一敌三
+
第一百九十章 戟法之威
+
第一百九十一章 解决
+
第一百九十二章 血狼帮之殇
+ + +
第一百九十三章 引爆阴煞之气
+
第一百九十四章 黑色阴丹
+
第一百九十五章 开启石符
+
第一百九十六章 大日雷体
+ + +
第一百九十七章 离别前的挑战
+
第一百九十八章 战城主
+
第一百九十九章 化蛟戟
+
第两百章 森林修行
+ + +
第两百零一章 引雷淬体
+
第两百零二章 吞噬雷霆
+
第两百零三章 小炎之危
+
第两百零四章 大阳郡狄家
+ + +
第两百零五章 狄腾
+
第两百零六章 雷源晶兽
+
第两百零七章 抢夺雷源
+
第两百零八章 大战造形境
+ + +
第两百零九章 炼化雷源
+
第两百一十章 山洞闭关
+
第两百一十一章 实力大涨
+
第两百一十二章 显威
+ + +
第两百一十三章 击溃
+
第两百一十四章 敲诈
+
第两百一十五章 迷雾森林
+
第两百一十六章 鹰之武馆
+ + +
第两百一十七章 迷雾豹鳄王
+
第两百一十八章 露底
+
第两百一十九章 大荒古碑
+
第两百二十章 血鹫武馆
+ + +
第两百二十一章 狠揍
+
第两百二十二章 美人献身
+
第两百二十三章 罗鹫
+
第两百二十四章 武斗台
+ + +
第两百二十五章 战造形境大成
+
第两百二十六章 魔猿变
+
第两百二十七章 击溃
+
第两百二十八章 邂逅
+ + +
第两百二十九章 魔猿精血
+
第两百三十章 远古龙猿
+
第两百三十一章 远古废涧
+
第两百三十二章 古剑门
+ + +
第两百三十三章 万兽果
+
第两百三十四章 驱虎吞狼
+
第两百三十五章 古剑憾龙猿
+
第两百三十六章 惊天大战
+ + +
第两百三十七章 精血到手
+
第两百三十八章 炼化龙猿精血
+
第两百三十九章 炼化成功
+
第两百四十章 肉搏
+ + +
第两百四十一章 大傀城
+
第两百四十二章 慕芊芊
+
第两百四十三章 拍卖会
+
第两百四十四章 蕴神蒲团
+ + +
第两百四十五章 程大师
+
第两百四十六章 进化的天鳞古戟
+
第两百四十七章 节外生枝
+
第两百四十八章 蒲团之谜
+ + +
第四百四十九章 元精之力
+
第两百五十章 围剿
+
第两百五十一章 灵符师
+
第两百五十二章 激斗华宗
+ + +
第两百五十三章 破甲
+
第两百五十四章 轰杀
+
第两百五十五章 大丰收
+
第两百五十六章 全力突破
+ + +
第两百五十七章 争分夺秒
+
第两百五十八章 追寻而至
+
第两百五十九章 硬憾造气大成
+
第两百六十章 震退
+ + +
第两百六十一章 黑衣青年
+
第两百六十二章 大荒古原
+
第两百六十三章 腾儡
+
第两百六十四章 再遇
+ + +
第两百六十五章 再战王炎
+
第两百六十六章 完虐
+
第两百六十七章 犀利言辞
+
第两百六十八章 封印消失
+ + +
第两百六十九章 古碑空间
+
第两百七十章 阴风炼体
+
第两百七十一章 石亭骸骨
+
第两百七十二章 冤家路窄
+ + +
第两百七十三章 断臂
+
第两百七十四章 核心地带
+
第两百七十五章 符傀巢穴
+
第两百七十六章 抢夺
+ + +
第两百七十七章 收服高等符傀
+
第两百七十八章 黑色祭坛
+
第两百七十九章 黑瞳老人
+
第两百八十章 封锁
+ + +
第两百八十一章 阴魔杀
+
第两百八十二章 高等符傀之力
+
第两百八十三章 造化武碑
+
第两百八十四章 十道蒲团
+ + +
第两百八十五章 抢夺席位
+
第两百八十六章 显凶威
+
第两百八十七章 强势擒获
+
第两百八十八章 占据
+ + +
第两百八十九章 大荒囚天指
+
第两百九十章 传承武学
+
第两百九十一章 造气境
+
第两百九十二章 宗派宝藏
+ + +
第两百九十三章 远古血蝠龙
+
第两百九十四章 斩杀血蝠龙
+
第两百九十五章 飞来横财
+
第两百九十六章 逃
+ + +
第两百九十七章 夺宝再逃
+
第两百九十八章 黑色符文
+
第两百九十九章 诅咒之力
+
第三百章 实力提升
+ + +
第三百零一章 上门挑衅
+
第三百零二章 强势出手
+
第三百零三章 血屠手曹震
+
第三百零四章 对战半步造化
+ + +
第三百零五章 九步震天踏
+
第三百零六章 安然而退
+
第三百零七章 情报
+
第三百零八章 紫影九破
+ + +
第三百零九章 阴傀城
+
第三百一十章 腾刹
+
第三百一十一章 黑瞳虚影
+
第三百一十二章 破阵
+ + +
第三百一十三章 造化境大成
+
第三百一十四章 抢了就跑
+
第三百一十五章 大乱
+
第三百一十六章 心狠手辣
+ + +
第三百一十七章 玄阴涧
+
第三百一十八章 无路可逃
+
第三百一十九章 绝境
+
第三百二十章 封印破解
+ + +
第三百二十一章 黑暗之界
+
第三百二十二章 祖符认可
+
第三百二十三章 高级灵符师
+
第三百二十四章 实力暴涨
+ + +
第三百二十五章 深入玄阴涧
+
第三百二十六章 危难
+
第三百二十七章 晋入半步造化
+
第三百二十八章 滔天杀意
+ + +
第三百二十九章 复仇
+
第三百三十章 大战造化大成
+
第三百三十一章 血战
+
第三百三十二章 煞气逼人
+ + +
第三百三十三章 祖符之威
+
第三百三十四章 灭宗
+
第三百三十五章 斩草除根
+
第三百三十六章 血灵傀
+ + +
第三百三十七章 下狠心
+
第三百三十八章 封印血灵傀
+
第三百三十九章 晋级的需求
+
第三百四十章 离开
+ + +
第三百四十一章 凑齐妖血
+
第三百四十二章 雷体大成
+
第三百四十三章 麻衣老人
+
第三百四十四章 险象环生
+ + +
第三百四十五章 大炎郡
+
第三百四十六章 族会!
+
第三百四十七章 强大的青檀
+
第三百四十八章 林动归来!
+ + +
第三百四十九章 滚下来
+
第三百五十章 何谓嚣张
+
第三百五十一章 一拳轰爆
+
第三百五十二章 给你一个字
+ + +
第三百五十三章 对战林琅天!
+
第三百五十四章 龙争虎斗
+
第三百五十五章 大天凰印!
+
第三百五十六章 底牌层出
+ + +
第三百五十七章 灵轮镜
+
第三百五十八章 拼命相搏
+
第三百五十九章 林梵
+
第三百六十章 落幕
+ + +
第三百六十一章 种子选拔
+
第三百六十二章 大炎王朝外的世界
+
第三百六十三章 族藏
+
第三百六十四章 暗袭
+ + +
第三百六十五章 神秘的黑色小山
+
第三百六十六章 重狱峰
+
第三百六十七章 造化境小成
+
第三百六十八章 给脸不要脸
+ + +
第三百六十九章 不留情面
+
第三百七十章 再次相对
+
第三百七十一章 残酷
+
第三百七十二章 赶往皇城
+ + +
第三百七十三章 天才云集
+
第三百七十四章 青衫莫凌
+
第三百七十五章 选拔开始
+
第三百七十六章 搬山二将
+ + +
第三百七十七章 皇普影
+
第三百七十八章 暗袭之术
+
第三百七十九章 破影
+
第三百八十章 最后的对手
+ + +
第三百八十一章 王钟
+
第三百八十二章 血魔修罗枪
+
第三百八十三章 苦战
+
第三百八十四章 血战
+ + +
第三百八十五章 名额
+
第三百八十六章 选拔落幕
+
第三百八十七章 暴怒的王雷
+
第三百八十八章 夜谈
+ + +
第三百八十九章 圣灵潭
+
第三百九十章 各施手段
+
第三百九十一章 抢夺能量
+
第三百九十二章 抢光
+ + +
第三百九十三章 林琅天体内的神秘存在
+
第三百九十四章 收获不小
+
第三百九十五章 骨枪
+
第三百九十六章 炼化天鳄骨枪
+ + +
第三百九十七章 血灵傀之变
+
第三百九十八章 进入远古战场!
+
第三百九十九章 陌生的空间
+
第四百章 聚集点
+ + +
第四百零一章 圣光王朝
+
第四百零二章 妖潮
+
第四百零三章 冲突
+
第四百零四章 屠戮
+ + +
第四百零五章 虎口夺食
+
第四百零六章 黎盛
+
第四百零七章 战造化境巅峰
+
第四百零八章 圣象崩天撞
+ + +
第四百零九章 五指动乾坤
+
第四百一十章 尽数轰杀
+
第四百一十一章 逼走
+
第四百一十二章 清点收获
+ + +
第四百一十三章 圣光王朝大师兄
+
第四百一十四章 情报
+
第四百一十五章 小涅盘金身
+
第四百一十六章 修炼金身
+ + +
第四百一十七章 麻烦上门
+
第四百一十八章 轰杀
+
第四百一十九章 前往阳城
+
第四百二十章 三人突破
+ + +
第四百二十一章 交易场
+
第四百二十二章 晋牧
+
第四百二十三章 对战半步涅盘
+
第四百二十四章 震慑
+ + +
第四百二十五章 名誉扫地
+
第四百二十六章 凑齐
+
第四百二十七章 天符灵树
+
第四百二十八章 净化血灵傀
+ + +
第四百二十九章 动身
+
第四百三十章 进入雷岩山脉
+
第四百三十一章 再遇妖潮
+
第四百三十二章 斩杀
+ + +
第四百三十三章 狠毒
+
第四百三十四章 收割
+
第四百三十五章 再度提升
+
第四百三十六章 讨债
+ + +
第四百三十七章 斩杀晋牧
+
第四百三十八章 凌志,柳元
+
第四百三十九章 雷岩谷
+
第四百四十章 两大高级王朝
+ + +
第四百四十一章 赌约
+
第四百四十二章 承让
+
第四百四十三章 暗流涌动
+
第四百四十四章 石殿
+ + +
第四百四十五章 树纹符文
+
第四百四十六章 底牌
+
第四百四十七章 主殿
+
第四百四十八章 机关
+ + +
第四百四十九章 石像
+
第四百五十章 红衣女子
+
第四百五十一章 穆红绫
+
第四百五十二章 变故
+ + +
第四百五十三章 夺舍
+
第四百五十四章 天符师
+
第四百五十五章 李盘
+
第四百五十六章 现身
+ + +
第四百五十七章 天符师的强大
+
第四百五十八章 杀手
+
第四百五十九章 麻烦
+
第四百六十章 算计
+ + +
第四百六十一章 来临
+
第四百六十二章 交手
+
第四百六十三章 雷蛇
+
第四百六十四章 吞噬之界
+ + +
第四百六十五章 肥羊
+
第四百六十六章 绑架勒索
+
第四百六十七章 冲击半步涅盘
+
第四百六十八章 取丹
+ + +
第四百六十九章 石轩
+
第四百七十章 一元涅盘
+
第四百七十一章 状况
+
第四百七十二章 体内阵法
+ + +
第四百七十三章 乾坤古阵
+
第四百七十四章 改变血脉
+
第四百七十五章 远古之地
+
第四百七十六章 大力裂地虎
+ + +
第四百七十七章 激斗
+
第四百七十八章 出手
+
第四百七十九章 惊退
+
第四百八十章 虎骨到手
+ + +
第四百八十一章 融合虎骨
+
第四百八十二章 脱胎换骨的小炎
+
第四百八十三章 三兄弟
+
第四百八十四章 远古之殿
+ + +
第四百八十五章 大殿
+
第四百八十六章 小炎之威
+
第四百八十七章 精元大吞掌
+
第四百八十八章 立威
+ + +
第四百八十九章 各方势力
+
第四百九十章 秘藏开启
+
第四百九十一章 金身舍利
+
第四百九十二章 丹河
+ + +
第四百九十三章 天鹰王朝
+
第四百九十四章 冲击涅盘
+
第四百九十五章 双劫齐至
+
第四百九十六章 厚积薄发
+ + +
第四百九十七章 实力大涨
+
第四百九十八章 摧枯拉朽
+
第四百九十九章 麻烦上门
+
第五百章 灵武学
+ + +
第五百零一章 灵武学
+
第五百零二章 再遇
+
第五百零三章 宗派遗迹
+
第五百零四章 威慑力
+ + +
第五百零五章 柳白
+
第五百零六章 天罡联盟
+
第五百零七章 涅盘焚天阵
+
第五百零八章 涅盘魔炎
+ + +
第五百零九章 神秘人
+
第五百一十章 八极宗
+
第五百一十一章 魔龙犬
+
第五百一十二章 掌印,拳印,指洞
+ + +
第五百一十三章 磅礴拳意
+
第五百一十四章 八极拳意
+
第五百一十五章 拳意之威
+
第五百一十六章 轰翻
+ + +
第五百一十七章 四玄宗遗迹
+
第五百一十八章 丹场
+
第五百一十九章 丹室
+
第五百二十章 生死转轮丹
+ + +
第五百二十一章 暴狼田震
+
第五百二十二章 小炎战田震
+
第五百二十三章 再遇
+
第五百二十四章 灵武学之斗
+ + +
第五百二十五章 群雄
+
第五百二十六章 青铜大门
+
第五百二十七章 动用底牌
+
第五百二十八章 召唤远古天鳄
+ + +
第五百二十九章 天鳄之威
+
第五百三十章 杀心
+
第五百三十一章 斩杀?
+
第五百三十二章 进入青铜大门
+ + +
第五百三十三章
+
第五百三十四章
+
第五百三十五章
+
第五百三十六章 神秘的青雉
+ + +
第五百三十七章 青天化龙诀
+
第五百三十八章 闭关
+
第五百三十九章 第三次涅盘劫
+
第五百四十章 对抗
+ + +
第五百四十一章 涅盘火雷珠
+
第五百四十二章 真正的天妖貂
+
第五百四十三章 神秘老人
+
第五百四十四章 出关
+ + +
第五百四十五章 大乾王朝
+
第五百四十六章 火将,山将
+
第五百四十七章 针锋相对
+
第五百四十八章 激战
+ + +
第五百四十九章 青龙撕天手
+
第五百五十章 败二将
+
第五百五十一章 离去
+
第五百五十二章 被盯上了
+ + +
第五百五十三章 小貂之力
+
第五百五十四章 夜遇
+
第五百五十五章 苏柔
+
第五百五十六章 出手
+ + +
第五百五十七章 狠手段
+
第五百五十八章 同行
+
第五百五十九章 涅盘碑
+
第五百六十章 涅盘碑测试
+ + +
第五百六十一章 常凌
+
第五百六十二章 包揽
+
第五百六十三章 万象拍卖会
+
第五百六十四章 罗通
+ + +
第五百六十五章 强势对碰
+
第五百六十六章 青龙指
+
第五百六十七章 风雨欲来
+
第五百六十八章 四大超级王朝
+ + +
第五百六十九章 拍卖会开始
+
第五百七十章 平衡灵果
+
第五百七十一章 天荒神牛
+
第五百七十二章 黑龙啸天印
+ + +
第五百七十三章 财力比拼
+
第五百七十四章 最终归属
+
第五百七十五章 即将对决
+
第五百七十六章 百朝大战,开启!
+ + +
第五百七十七章 对决
+
第五百七十八章 血战
+
第五百七十七章 天阶灵宝的威力
+
第五百七十八章 惊天动地
+ + +
第五百七十九章 夺宝
+
第五百八十章 败亡
+
第五百八十一章 序幕拉开
+
第五百八十二章 死灵将
+ + +
第五百八十三章 二段封印
+
第五百八十四章 苏醒
+
第五百八十五章 赶尽杀绝
+
第五百八十六章 实力精进
+ + +
第五百八十七章 挺进深处
+
第五百八十八章 敢不敢
+
第五百八十九章 抗
+
第五百九十章 地煞联盟
+ + +
第五百九十一章 对头
+
第五百九十二章 找上门来
+
第五百九十三章 萧山
+
第五百九十四章 龙灵战朱厌
+ + +
第五百九十五章 横扫
+
第五百九十六章 再遇
+
第五百九十七章 蓝樱
+
第五百九十八章 七大超级宗派
+ + +
第五百九十九章 合作
+
第六百章 应战
+
第六百零一章 宋家三魔
+
第六百零二章 怪异之举
+ + +
第六百零三章 变态啊
+
第六百零四章 四元涅盘境
+
第六百零五章 饕鬄凶灵
+
第六百零六章 吞食与吞噬
+ + +
第六百零七章 胜负
+
第六百零八章 你还有力量么?
+
第六百零九章 惨
+
第六百一十章 秦天
+ + +
第六百一十一章 百朝山开
+
第六百一十二章 上山
+
第六百一十三章 八大超级宗派
+
第六百一十四章 熟面孔
+ + +
第六百一十五章 涅盘金榜之战
+
第六百一十六章 合作
+
第六百一十七章 再战林琅天
+
第六百一十八章 聚武灵
+ + +
第六百一十九章 手段尽施
+
第六百二十章 不动青龙钟
+
第六百二十一章 暴力
+
第六百二十二章 斩杀林琅天
+ + +
第六百二十三章 斩尽杀绝
+
第六百二十四章 争夺空间
+
第六百二十五章 曹羽
+
第六百二十六章 出手
+ + +
第六百二十七章 底牌尽出
+
第六百二十八章 叠加
+
第六百二十九章 三劫叠加
+
第六百三十章 雷劫憾阵
+ + +
第六百三十一章 解困
+
第六百三十二章 再见绫清竹?
+
第六百三十三章 四年
+
第六百三十四章 来历
+ + +
第六百三十五章 挑选宗派
+
第六百三十六章 加入道宗
+
第六百三十七章 百朝大战落幕
+
第六百三十八章 震撼大炎
+ + +
第六百三十九章 道域,道宗!
+
第六百四十章 四大奇经
+
第六百四十一章 择殿
+
第六百四十二章 赏赐
+ + +
第六百四十三章 指教
+
第六百四十四章 交手
+
第六百四十五章 荒刀
+
第六百四十六章
+ + +
第六百四十七章 涅盘金气
+
第六百四十八章 丹河之底
+
第六百四十九章 动静
+
第六百五十章 轰动
+ + +
第六百五十一章 龙元轮
+
第六百五十二章 五元涅盘劫
+
第六百五十三章 破河而出
+
第六百五十四章 亲传大弟子
+ + +
第六百五十五章 蒋浩的阻拦
+
第六百五十六章 武学殿
+
第六百五十七章 荒决
+
第六百五十八章 荒石
+ + +
第六百五十九章 凝聚荒种
+
第六百六十章 四座石碑
+
第六百六十一章 荒芜妖眼
+
第六百六十二章 荒
+ + +
第六百六十三章 成功与否?
+
第六百六十四章 月比
+
第六百六十五章 激斗蒋浩
+
第六百六十六章 大星罡拳
+ + +
第六百六十七章 妖眼之力
+
第六百六十八章 第五位亲传大弟子
+
第六百六十九章 月谈
+
第六百七十章 宁静
+ + +
第六百七十一章 出宗
+
第六百七十二章 血岩地
+
第六百七十三章 仙元古树
+
第六百七十四章 猿王
+ + +
第六百七十五章 斗猿王
+
第六百七十六章 斩杀
+
第六百七十七章 动静
+
第六百七十八章 不妙
+ + +
第六百七十九章 麻烦的局面
+
第六百八十章 激斗屠夫
+
第六百八十一章 荒兽之灵
+
第六百八十二章 撤退
+ + +
第六百八十三章 无相菩提音
+
第六百八十四章 救人
+
第六百八十五章 恩怨
+
第六百八十六章 血斗
+ + +
第六百八十七章 吞噬仙元古果
+
第六百八十八章 斩杀苏雷
+
第六百八十九章 魔元咒体
+
第六百九十章 重伤
+ + +
第六百九十一章 休养
+
第六百九十二章 应笑笑,青叶
+
第六百九十三章 道宗掌教
+
第六百九十四章 锁灵阵
+ + +
第六百九十五章 再渡双劫
+
第六百九十六章 大荒芜碑
+
第六百九十七章 波动
+
第六百九十八章 荒芜
+ + +
第六百九十九章 你病了
+
第七百章 破局
+
第七百零一章 未知生物
+
第七百零二章 参悟大荒芜经
+ + +
第七百零三章 成功
+
第七百零四章 拜山
+
第七百零五章 洪崖洞
+
第七百零六章 暴力
+ + +
第七百零七章 洪崖洞经
+
第七百零八章 强化的大荒囚天手
+
第七百零九章 弹琴的少女
+
第七百一十章 王阎
+ + +
第七百一十一章 对恃
+
第七百一十二章 殿试开始
+
第七百一十三章 对战应欢欢
+
第七百一十四章 对手
+ + +
第七百一十五章 顶尖交锋
+
第七百一十六章 激战
+
第七百一十七章 地龙封神印
+
第七百一十八章 龙翼
+ + +
第七百一十九章 最顶尖的较量
+
第七百二十章 王阎对应笑笑
+
第七百二十一章 天皇经的对碰
+
第七百二十二章 出手
+ + +
第七百二十三章 龙争虎斗
+
第七百二十四章 黑魔鉴VS大荒芜经
+
第七百二十五章 惨烈
+
第七百二十六章 胜败
+ + +
第七百二十七章 指挥权归属
+
第七百二十八章 选宝
+
第七百二十九章 静止之牌
+
第七百三十章 妖灵烙印的动静
+ + +
第七百三十一章 借琴
+
第七百三十二章 三人再聚
+
第七百三十三章 地心孕神涎
+
第七百三十四章 魔音山
+ + +
第七百三十五章 突来之人
+
第七百三十六章 变故
+
第七百三十七章 局势转变
+
第七百三十八章 不过如此
+ + +
第七百三十九章 斩草除根
+
第七百四十章 天妖貂的力量
+
第七百四十一章 收获颇丰
+
第七百四十二章 斗法
+ + +
第七百四十三章 抹除
+
第七百四十四章 破丹孕神
+
第七百四十五章 实力大涨
+
第七百四十六章 轮回者
+ + +
第七百四十七章 回宗
+
第七百四十八章 谈话
+
第七百四十九章 可还记得
+
第七百五十章 动身
+ + +
第七百五十一章 异魔城
+
第七百五十二章 冲突
+
第七百五十三章 如鹰如隼
+
第七百五十四章 滑稽的交手
+ + +
第七百五十五章 认识一下
+
第七百五十六章 五年后的见面
+
第七百五十七章 想死?
+
第七百五十八章 焚天古藏
+ + +
第七百五十九章 妖孽云集
+
第七百六十章 针锋相对
+
第七百六十一章 异魔域,开启
+
第七百六十二章 生玄骨珠
+ + +
第七百六十三章 变故
+
第七百六十四章 苦战魔尸
+
第七百六十五章 操控魔尸
+
第七百六十六章 骚扰
+ + +
第七百六十七章 古藏信息
+
第七百六十八章 赶往古藏
+
第七百六十九章 借刀杀人
+
第七百七十章 兄妹相见
+ + +
第七百七十一章 青檀
+
第七百七十二章 战书
+
第七百七十三章 激战雷千
+
第七百七十四章 雷帝典
+ + +
第七百七十五章 逆转之威
+
第七百七十六章 对恃
+
第七百七十七章 焚天古藏开启
+
第七百七十八章 诡异的空间
+ + +
第七百七十九章 中枢
+
第七百八十章 确定
+
第七百八十一章 变故
+
第七百八十二章 鼎炉
+ + +
第七百八十三章 赤袍人
+
第七百八十四章 赤袍对黑雾
+
第七百八十五章 镇压
+
第七百八十六章 焚天
+ + +
第七百八十七章 炼化
+
第七百八十八章 八元涅盘境
+
第七百八十九章 太清仙池
+
第七百九十章 杨氏兄弟
+ + +
第七百九十一章 武帝典
+
第七百九十二章 焚天阵之威
+
第七百九十三章 武帝
+
第七百九十四章 池底变故
+ + +
第七百九十五章 后果
+
第七百九十六章 动手
+
第七百九十七章 开战
+
第七百九十八章 恩怨
+ + +
第七百九十九章 弟子之战
+
第八百章 混战
+
第八百零一章 元苍的灵印
+
第八百零二章 两女联手
+ + +
第八百零三章 惨烈
+
第八百零四章 能耐
+
第八百零五章 顶尖交锋
+
第八百零六章 再现大荒芜经
+ + +
第八百零七章 激斗元苍
+
第八百零八章 焚天鼎之威
+
第八百零九章 局势转换
+
第八百一十章 疯子
+ + +
第八百一十一章 荒芜石珠
+
第八百一十二章 惨胜
+
第八百一十三章 落幕
+
第八百一十四章 震动
+ + +
第八百一十五章 归来
+
第八百一十六章 纠纷
+
第八百一十七章 再聚首
+
第八百一十八章 小貂之威
+ + +
第八百一十九章 以一敌六
+
第八百二十章 人元子
+
第八百二十一章 惨烈
+
第八百二十二章 惨败的三兄弟
+ + +
第八百二十三章 一份情
+
第八百二十四章 顶尖强者云集
+
第八百二十五章 退宗
+
第八百二十七章(上) 拼命
+ + +
第八百二十七章(下) 空间挪移
+
第八百二十八章
+
第八百二十九章 逃离
+
第八百三十章 他会回来的
+ + +
第八百三十一章 陌生的地方
+
第八百三十二章 乱魔海,天风海域
+
第八百三十三章 生生玄灵果
+
第八百三十四章 玄元丹
+ + +
第八百三十五章 夜袭
+
第八百三十六章 仙符师
+
第八百三十七章 报酬
+
第八百三十八章 冲击九元涅盘境
+ + +
第八百三十九章 玄灵山
+
第八百四十章 震慑
+
第八百四十一章 各方云集
+
第八百四十二章
+ + +
第八百四十三章 青龙之力
+
第八百四十四章 三头魔蛟
+
第八百四十五章 混战
+
第八百四十六章 各施手段
+ + +
第八百四十七章 到手
+
第八百四十八章 局势
+
第八百四十九章 吸进鼎炉
+
第八百五十章 峥嵘
+ + +
第八百五十一章 抹杀
+
第八百五十二章 地心生灵浆
+
第八百五十三章 进湖
+
第八百五十四章 岩浆之后
+ + +
第八百五十五章 虎口夺食
+
第八百五十六章 神秘的空间
+
第八百五十七章 令牌
+
第八百五十八章 外援
+ + +
第八百五十九章 生玄境
+
第八百六十章 洪荒塔
+
第三百六十一章 动身
+
第三百六十二章 武会岛
+ + +
第八百六十三章 争端
+
第八百六十四章 承让
+
第八百六十五章 油盐不进
+
第八百六十六章 合作
+ + +
第八百六十七章 冤家路窄
+
第八百六十八章 武会
+
第八百六十九章 分配
+
第八百七十章 激斗苏岩
+ + +
第八百七十一章 一掌
+
第八百七十二章 获胜
+
第八百七十三章 修罗模式
+
第八百七十四章 挑战
+ + +
第八百七十五章 武帝怒
+
第八百七十六章 强势
+
第八百七十七章 初步接触
+
第八百七十八章 海
+ + +
第八百七十九章 此路,不通
+
第八百八十章 青龙武装
+
第八百八十一章 青龙战修罗
+
第八百八十二章 修罗地煞狱
+ + +
第八百八十三章 底牌频出
+
第八百八十四章 胜
+
第八百八十五章 落幕
+
第八百八十六章 进入洪荒塔
+ + +
第八百八十七章 如海般的洪荒之气
+
第八百八十八章 获益匪浅
+
第八百八十九章 紫金之皮
+
第八百九十章 祖石之灵
+ + +
第八百九十一章 麻烦上门
+
第八百九十二章 邪骨老人
+
第八百九十三章 设计
+
第八百九十四章 离开
+ + +
第八百九十五章 斗邪骨
+
第八百九十六章 炎神古牌之力
+
第八百九十七章 重伤
+
第八百九十八章 血魔鲨族
+ + +
第八百九十九章 青衣女童
+
第九百章 慕灵珊
+
第九百零一章 救人
+
第九百零二章 交手
+ + +
第九百零三章 先下手
+
第九百零四章 屠戮
+
第九百零五章 海上激战
+
第九百零六章 魔鲨之牙
+ + +
第九百零七章 斩草除根
+
第九百零八章 天商城
+
第九百零九章 唐冬灵
+
第九百一十章 炼制焚天门
+ + +
第九百一十一章 完整的焚天鼎
+
第九百一十二章 天商拍卖会
+
第九百一十三章 对立
+
第九百一十四章 吞噬天尸
+ + +
第九百一十五章 竞价
+
第九百一十六章 压箱底之物?
+
第九百一十七章 雷霆祖符的线索
+
第九百一十八章 争夺银塔
+ + +
第九百一十九章 操控天尸
+
第九百二十章 好戏
+
第九百二十一章 变故
+
第九百二十二章 动手
+ + +
第九百二十三章 焚天门之威
+
第九百二十四章 夺塔而走
+
第九百二十五章 追兵
+
第九百二十六章 天雷海域
+ + +
第九百二十七章 途遇
+
第九百二十八章 无轩
+
第九百二十九章 两大转轮境
+
第九百三十章 薄礼
+ + +
第九百三十一章 夜谈
+
第九百三十二章 抵达
+
第九百三十三章 算计
+
第九百三十四章 杀鸡儆猴
+ + +
第九百三十五章 水深
+
第九百三十六章 进入天雷海域
+
第九百三十七章 凶险的天雷海域
+
第九百三十八章 洞府开启
+ + +
第九百三十九章 雷光战场
+
第九百四十章 洞府之内
+
第九百四十一章 追杀
+
第九百四十二章 抹杀
+ + +
第九百四十三章 神秘红袍人
+
第九百四十四章 雷霆之心
+
第九百四十五章 交手
+
第九百四十六章 热闹
+ + +
第九百四十七章 雷岩沟壑
+
第九百四十八章 收取
+
第九百四十九章 湖底混战
+
第九百五十章 拖下水
+ + +
第九百五十一章 激战庞昊
+
第九百五十二章 凶狠
+
第九百五十三章 异魔?
+
第九百五十四章 退避
+ + +
第九百五十五章 帮
+
第九百五十六章 三大神物
+
第九百五十七章 抹除魔纹
+
第九百五十八章 左费
+ + +
第九百五十九章 吸收
+
第九百六十章 雷殿
+
第九百六十一章 强者汇聚
+
第九百六十二章 九幽镇灵阵
+ + +
第九百六十三章 引尸
+
第九百六十四章(上) 断手
+
第九百六十四章(下) 雷帝权杖
+
第九百六十五章 争夺
+ + +
第九百六十六章 降服
+
第九百六十七章 上一届的元门三小王
+
第九百六十八章 摩罗
+
第九百六十九章 驱逐
+ + +
第九百七十章 雷界
+
第九百七十一章 联手诛魔
+
第九百七十二章 三大祖符
+
第九百七十三章 抹杀异魔王
+ + +
第九百七十四章 争执
+
第九百七十五章 倾尽手段
+
第九百七十六章 血拼到底
+
第九百七十七章 我赢了
+ + +
第九百七十八章 诱饵
+
第九百七十九章 我自巍然
+
第九百八十章 十万雷霆铸雷身
+
第九百八十一章 炼化雷霆祖符
+ + +
第九百八十二章 实力大进
+
第九百八十三章(上) 蹲守
+
第九百八十三章(下) 救人
+
第九百八十四章 诛杀
+ + +
第九百八十五章 狠手段
+
第九百八十六章 魔迹
+
第九百八十七章 除魔
+
第九百八十八章 两大祖符
+ + +
第九百八十九章 诛杀
+
第九百九十章 解决
+
第九百九十一章 震动
+
第九百九十二章 新秀榜
+ + +
第九百九十三章 火炎城
+
第九百九十四章 熟人
+
第九百九十五章 冲突
+
第九百九十六章 交手
+ + +
第九百九十七章 唐心莲
+
第九百九十八章 水深
+
第九百九十九章 报我的名
+
第一零零零章 大赛前的平静
+ + +
第一千零一章 开启
+
第一千零二章 血之斩头卫
+
第一千零三章 一路向前
+
第一千零四章 遇上
+ + +
第一千零五章 初次交手
+
第一千零六章 无量山
+
第一千零七章 登山
+
第一千零八章 鲨力
+ + +
第一千零九章 登顶
+
第一千一十章 顶上之争
+
第一千一十一章 巅峰之战
+
第一千一十二章 隐忍待发
+ + +
第一千一十三章 青锋出鞘
+
第一千一十四章 斗两魔
+
第一千一十五章 手段尽出
+
第一千一十六章 三重攻势
+ + +
第一千一十七章 断臂
+
第一千一十八章 各施手段
+
第一千一十九章 三百道
+
第一千二十章 破镜
+ + +
第一千二十一章 柔软
+
第一千二十二章 魔现
+
第一千二十三章 天冥王
+
第一千二十四章 青雉再现
+ + +
第一千二十五章 炎神殿的统帅
+
第一千二十六章 恐怖的女孩
+
第一千二十七章
+
第一千二十八章 冲击
+ + +
第一千二十九章 镇压
+
第一千三十章 灭王天盘
+
第一千三十一章 生死祖符
+
第一千三十二章 灭王天盘
+ + +
第一千三十三章 魔狱
+
第一千三十四章 踪迹
+
第一千三十五章 死炎灵池
+
第一千三十六章 死气冲刷
+ + +
第一千三十七章 最后一道
+
第一千三十八章 离开
+
第一千三十九章 暗云涌动
+
第一千四十章 兽战域
+ + +
第一千四十一章 抢我的烤肉
+
第一千四十二章 带走
+
第一千四十三章 垫脚石
+
第一千四十四章 曹赢
+ + +
第一千四十五章 露峥嵘
+
第一千四十六章 达到目的
+
第一千四十七章 抵达
+
第一千四十八章 九尾寨
+ + +
第一千四十九章 秦刚与蒙山
+
第一千五十章 兄弟相聚
+
第一千五十一章 炎将
+
第一千五十二章 小炎的经历
+ + +
第一千五十三章 相谈
+
第一千五十四章 秘辛
+
第一千五十五章 祖魂殿
+
第一千五十六章 九尾灵狐
+ + +
第一千五十七章 传承
+
第一千五十八章 吞噬神殿
+
第一千五十九章 希望
+
第一千六十章 还有怀疑吗
+ + +
第一千六十一章慑服
+
第一千六十二章 雷渊山脉
+
第一千六十三章 妖帅徐钟
+
第一千六十四章 各自的准备
+ + +
第一千六十五章 斗妖帅
+
第一千六十六章 雷渊山之战
+
第一千六十七章 发狂
+
第一千六十八章 抹杀
+ + +
第一千六十九章 易主
+
第一千七十章 精血传承
+
第一千七十一章 神物宝库
+
第一千七十二章 无间神狱盘
+ + +
第一千七十三章 锤锻精神
+
第一千七十四章 血龙殿
+
第一千七十五章 两大统领
+
第一千七十六章 天龙妖帅
+ + +
第一千七十七章 风波暂息
+
第一千七十八章 暴风雨前的宁静
+
第一千七十九章 神物山脉
+
第一千八十章 投鼠忌器
+ + +
第一千八十一章 宝库出现
+
第一千八十二章 取宝
+
第一千八十三章 各施手段
+
第一千八十四章 玄天殿内
+ + +
第一千八十五章 万魔蚀阵
+
第一千八十六章 联手
+
第一千八十七章 背水一战
+
第一千八十八章 战转轮
+ + +
第一千八十九章 玄天殿
+
第一千九十章 再说一次
+
第一千九十一章 三兄弟,终聚首
+
第一千九十二章 霸道的天妖貂
+ + +
第一千九十三章 小貂斗天龙
+
第一千九十四章 龙族
+
第一千九十五章 对恃
+
第一千九十六章 龙族的问题
+ + +
第一千九十七章 前往龙族
+
第一千九十八章 龙族
+
第一千九十九章 龙族的麻烦
+
第一千一百章 棘手
+ + +
第一千一百零一章 镇魔狱
+
第一千一百零二章 黑暗之主
+
第一千一百零三章 解决魔海
+
第一千一百零四章 名额
+ + +
第一千一百零五章 严山
+
第一千一百零六章 龙骨
+
第一千一百零七章 化龙潭开启
+
第一千一百零八章 化龙骨
+ + +
第一千一百零九章 埋骨之地
+
第一千一百一十章 远古龙骨
+
第一千一百一十一章 六指圣龙帝
+
第一千一百一十二章 帮手
+ + +
第一千一百一十三章 巅峰交手
+
第一千一百一十四章 洪荒龙骨
+
第一千一百一十五章 刑罚长老
+
第一千一百一十六章 离开
+ + +
第一千一百一十七章 邙山
+
第一千一百一十八章 联手
+
第一千一百一十九章 四象宫
+
第一千一百二十章 妖兽古原
+ + +
第一千一百二十一章 天擂台
+
第一千一百二十二章 连败
+
第一千一百二十三章 神锤憾玄武
+
第一千一百二十四章 最后一场
+ + +
第一千一百二十五章 激战罗通
+
第一千一百二十六章 九凤化生光
+
第一千一百二十七章 惨烈
+
第一千一百二十八章 结束
+ + +
第一千一百二十九章 大整顿
+
第一千一百三十章 小貂的麻烦
+
第一千一百三十一章 魔狱再现
+
第一千一百三十二章 天洞
+ + +
第一千一百三十三章 永恒幻魔花
+
第一千一百三十四章 苏醒
+
第一千一百三十五章 对决
+
第一千一百三十六章 孰强孰弱
+ + +
第一千一百三十七章 昊九幽的手段
+
第一千一百三十八章 抓魔
+
第一千一百三十九章 异魔王再现
+
第一千一百四十章 大礼
+ + +
第一千一百四十一章 出手
+
第一千一百四十二章 荒芜再现
+
第一千一百四十三章 永恒花魔身
+
第一千一百四十四章 祖符之手
+ + +
第一千一百四十五章 解局
+
第一千一百四十六章 冲击符宗
+
第一千一百四十七章 炼狱
+
第一千一百四十八章 化茧
+ + +
第一千一百四十九章 守关者
+
第一千一百五十章 晋入符宗
+
第一千一百五十一章 出关
+
第一千一百五十二章 投诚
+ + +
第一千一百五十三章 打压
+
第一千一百五十四章 一剑败双雄
+
第一千一百五十五章 震慑
+
第一千一百五十六章 妖域震动
+ + +
第一千一百五十七章 群强云集
+
第一千一百五十八章 柳青
+
第一千一百五十九章 鲲灵
+
第一千一百六十章 进荒原
+ + +
第一千一百六十一章 抵达
+
第一千一百六十二章 黑暗圣虎
+
第一千一百六十三章 三大虎族
+
第一千一百六十四章 以一敌二
+ + +
第一千一百六十五章 孤峰上的大殿
+
第一千一百六十六章 神秘黑影
+
第一千一百六十七章 闯关
+
第一千一百六十八章 九峰
+ + +
第一千一百六十九章 黑暗之中
+
第一千一百七十章 两股吞噬之力
+
第一千一百七十一章 吞噬之主
+
第一千一百七十二章 传承之秘
+ + +
第一千一百七十三章 借身斩魔
+
第一千一百七十四章 恐怖的吞噬之主
+
第一千一百七十五章 福泽
+
第一千一百七十六章 三重轮回劫
+ + +
第一千一百七十七章 轮回之海
+
第一千一百七十八章 战争
+
第一千一百七十九章 杀回去
+
第一千一百八十章 调遣人马
+ + +
第一千一百八十一章 重回东玄域
+
第一千一百八十二章 东玄域的局势
+
第一千一百八十三章 归来
+
第一千一百八十四章 打
+ + +
第一千一百八十五章 传奇
+
第一千一百八十六章 再见应欢欢
+
第一千一百八十七章 壮我道宗!
+
第一千一百八十八章 归宗
+ + +
第一千一百八十九章 表现
+
第一千一百九十章 进碑
+
第一千一百九十一章 联手斩魔
+
第一千一百九十二章 撞车
+ + +
第一千一百九十三章 判断失误
+
第一千一百九十四章 太清宫之难
+
第一千一百九十五章 太上宫
+
第一千一百九十六章 孤寂芳影,一人迎敌
+ + +
第一千一百九十七章 没人能伤你
+
第一千一百九十八章 小心点
+
第一千一百九十九章 讨债开始
+
第一千两百章 显威
+ + +
第一千两百零一章 第八道祖符
+
第一千两百零二章 空间祖符
+
第一千两百零三章 诛元盟
+
第一千两百零四章 汇聚,决战来临
+ + +
第一千两百零五章 元门之外
+
第一千两百零六章 周通
+
第一千两百零七章 林动vs周通
+
第一千两百零八章 魔皇锁
+ + +
第一千两百零九章 解救
+
第一千两百一十章 盛大魔宴
+
第一千两百一十一章 龙,虎,貂
+
第一千两百一十二章 斗魔
+ + +
第一千两百一十三章 雪耻之战
+
第一千两百一十四章 祖符之眼
+
第一千两百一十五章 斩杀三巨头
+
第一千两百一十六章 两女联手
+ + +
第一千两百一十七章 四王殿
+
第一千两百一十八章 炎主
+
第一千两百一十九章 树静风止
+
第一千两百二十章 大符宗
+ + +
第一千两百二十一章 怒斗炎主
+
第一千两百二十二章 大雪初晴
+
第一千两百二十三章 相谈
+
第一千两百二十四章 太上感应诀
+ + +
第一千两百二十五章 山顶之谈
+
第一千两百二十六章 千万大山
+
第一千两百二十七章 再遇辰傀
+
第一千两百二十八章 青檀之事
+ + +
第一千两百二十九章 黑暗之城
+
第一千两百三十章 逼宫
+
第一千两百三十一章 相见
+
第一千两百三十二章 显威
+ + +
第一千两百三十三章 镰灵
+
第一千两百三十四章 手段
+
第一千两百三十五章 魔袭而来
+
第一千两百三十六章 七王殿
+ + +
第一千两百三十七章 魔皇甲
+
第一千两百三十八章 力战
+
第一千两百三十九章 修炼之法
+
第一千两百四十章 感应
+ + +
第一千两百四十一章 雷弓黑箭
+
第一千两百四十二章 雷主
+
第一千两百四十三章 魔狱之事
+
第一千两百四十四章 再回异魔域
+ + +
第一千两百四十五章 唤醒焚天
+
第一千两百四十六章 相聚
+
第一千两百四十七章 安宁
+
第一千两百四十八章 鹰宗
+ + +
第一千两百四十九章 故人
+
第一千两百五十章 碑中之魔
+
第一千两百五十一章 九王殿
+
第一千两百五十二章 诛杀
+ + +
第一千两百五十三章 差之丝毫
+
第一千两百五十四章 回宗
+
第一千两百五十五章 师徒相聚
+
第一千两百五十六章 位面裂缝
+ + +
第一千两百五十七章 晋入轮回
+
第一千两百五十八章 动荡之始
+
第一千两百五十九章 再至乱魔海
+
第一千两百六十章 万魔围岛
+ + +
第一千两百六十一章 熟人
+
第一千两百六十二章 洪荒之主
+
第一千两百六十三章 大战来临
+
第一千两百六十四章 再遇七王殿
+ + +
第一千两百六十五章 血战
+
第一千两百六十六章 魔皇虚影
+
第一千两百六十七章 混沌之箭
+
第一千两百六十八章 应欢欢出手
+ + +
第一千两百六十九章 空间之主
+
第一千两百七十章 诸强汇聚
+
第一千两百七十一章 巅峰对恃
+
第一千两百七十二章 祖宫阙
+ + +
第一千两百七十三章 联盟
+
第一千两百七十四章 大殿盛宴
+
第一千两百七十五章 大会
+
第一千两百七十六章 开启祖宫阙
+ + +
第一千两百七十七章 修炼之路
+
第一千两百七十八章 动荡
+
第一千两百七十九章 凝聚神宫
+
第一千两百八十章 三大联盟
+ + +
第一千两百八十一章 天王殿
+
第一千两百八十二章 一瞬十年
+
第一千两百八十三章 青雉战魔
+
第一千两百八十四章 平定乱魔海
+ + +
第一千两百八十五章 四玄域联盟
+
第一千两百八十六章 争吵
+
第一千两百八十七章 生死之主
+
第一千两百八十八章 大军齐聚
+ + +
第一千两百八十九章 进攻西玄域
+
第一千两百九十章 西玄大沙漠
+
第一千两百九十一章 天地大战
+
第一千两百九十二章 激斗三王殿
+ + +
第一千两百九十三章 魔皇之像
+
第一千两百九十四章 魔皇之手
+
第一千两百九十五章 自己来守护
+
第一千两百九十六章 位面裂缝
+ + +
第一千两百九十七章 后手
+
第一千两百九十八章 抉择
+
第一千两百九十九章 青阳镇
+
第一千三百章 一年
+ + +
第一千三百零一章 成功与否
+
第一千三百零二章 祈愿
+
第一千三百零三章 轮回
+
第一千三百零四章 封印破碎
+ + +
第一千三百零五章 晋入祖境
+
第一千三百零六章 最后一战
+
第一千三百零七章 我要把你找回来
+
结局感言以及新书
+ + +
大结局活动,1744,欢迎大家。
+
应欢欢篇
+
绫清竹篇
+
新书大主宰已发。
+
+
+
+ +
+ + + + """ + res = AttrField("href", css_select='#list > dl > dd:nth-child(1) > a').extract(html) + print(res) diff --git a/smart/item.py b/smart/item.py new file mode 100644 index 0000000..1bce3b3 --- /dev/null +++ b/smart/item.py @@ -0,0 +1,196 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: item +# Author: liangbaikai +# Date: 2020/12/31 +# Desc: there is a python file description +# ------------------------------------------------------------------ +from __future__ import annotations + +import copy +import inspect +from typing import Any, Union + +from lxml import etree +from ruia.exceptions import InvalidFuncType + +from smart.field import BaseField, RegexField, FuncField + + +class ItemMeta(type): + """ + Metaclass for an item + """ + + def __new__(cls, name, bases, attrs): + __fields = dict( + { + (field_name, attrs.get(field_name)) + for field_name, object in list(attrs.items()) + if not field_name.startswith("_") and not inspect.isfunction(object) + # if isinstance(object, BaseField) + # and not field_name.startswith("_") + } + ) + attrs["__fields"] = __fields + new_class = type.__new__(cls, name, bases, attrs) + return new_class + + +# class Item(metaclass=ItemMeta): +# """ +# Item class for each item +# """ +# +# def __init__(self, source): +# self.__source = source +# results = self.__get_item() or {} +# self.__dict__.update(results) +# +# def to_dict(self): +# dict___items = {k: v for k, v in self.__dict__.items() if not k.startswith("_")} +# return dict___items +# +# def extract(self, key, other_source): +# if not key or not other_source: +# return None +# cls = self.__class__ +# fields = getattr(cls, "__fields") +# if key not in fields.keys(): +# return None +# for k, v in fields.items(): +# if isinstance(v, BaseField): +# value = v.extract(other_source) +# self.__dict__.update(key=value) +# return value +# +# def __get_item( +# self, +# ) -> Any: +# cls = self.__class__ +# fields = getattr(cls, "__fields") +# dict = {} +# for k, v in fields.items(): +# if isinstance(v, BaseField): +# value = v.extract(self.__source) +# else: +# value = v +# dict.setdefault(k, value) +# for k, v in cls.__dict__.items(): +# if k.startswith("_"): +# continue +# dict.setdefault(k, v) +# return dict +# +# def __getitem__(self, key): +# return self.__dict__[key] +# +# def __setitem__(self, key, value): +# if key in self.__dict__.keys(): +# self.__dict__[key] = value +# else: +# raise KeyError("%s does not support field: %s" % +# (self.__class__.__name__, key)) + + +class Item(metaclass=ItemMeta): + """ + Item class for each item + """ + + def __init__(self): + self.ignore_item = False + self.results = {} + + @classmethod + def _get_html(cls, html: str = "", **kwargs): + if html: + return etree.HTML(html) + else: + raise ValueError("") + item_ins = cls() + fields_dict = getattr(item_ins, "__fields", {}) + for field_name, field_value in fields_dict.items(): + if not field_name.startswith("target_"): + clean_method = getattr(item_ins, f"clean_{field_name}", None) + if isinstance(field_value, BaseField): + value = field_value.extract(html_etree) + else: + value = getattr(item_ins, field_name) + if clean_method is not None and callable(clean_method): + try: + value = clean_method(value) + except Exception: + item_ins.ignore_item = True + + setattr(item_ins, field_name, value) + item_ins.results[field_name] = value + return item_ins + + @classmethod + def get_item( + cls, + html: str = "", + **kwargs, + ) -> Any: + return cls._parse_html(html_etree=html) + + @classmethod + def get_items( + cls, + html: Union[str, dict, etree._Element], + **kwargs, + ): + items_field = getattr(cls, "__fields", {}).get("target_item", None) + if items_field: + items_field.many = True + items_html_etree = items_field.extract( + html=html + ) + if items_html_etree: + for each_html_etree in items_html_etree: + item = cls._parse_html(html_etree=each_html_etree) + if not item.ignore_item: + yield item + else: + value_error_info = "" + raise ValueError(value_error_info) + else: + raise ValueError( + "" + ) + + def __repr__(self): + return "" % (self.results,) + + def __getitem__(self, key): + return self.results.get(key) + + def __setitem__(self, key, value): + if key in self.results.keys(): + self.results[key] = value + else: + raise KeyError("%s does not support field: %s" % + (self.__class__.__name__, key)) + + def __setattr__(self, name, value): + if self.__dict__.get("results") is None: + self.__dict__[name] = value + else: + self.__dict__["results"][name] = value + + def __getattr__(self, item): + return self.__getitem__(item) + + def __iter__(self): + return self + + def __next__(self): + raise StopIteration() diff --git a/smart/log.py b/smart/log.py new file mode 100644 index 0000000..a9a1230 --- /dev/null +++ b/smart/log.py @@ -0,0 +1,194 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: log +# Author: liangbaikai +# Date: 2020/12/29 +# Desc: there is a log py for smart-framework +# ------------------------------------------------------------------ +import logging +import os +import sys +from logging.handlers import BaseRotatingHandler +from traceback import format_exception + +from smart.setting import gloable_setting_dict + +LOG_FORMAT = "process %(process)d|thread %(threadName)s|%(asctime)s|%(filename)s|%(funcName)s|line:%(lineno)d|%(levelname)s: %(message)s" +CONSOLE_LOG_FORMAT = "%(colorName)sprocess %(process)d|thread %(threadName)s|%(asctime)s|%(filename)s|%(funcName)s|line:%(lineno)d|%(levelname)s: %(message)s %(colorNameSuffix)s" + +PRINT_EXCEPTION_DETAILS = True + + +# 重写 RotatingFileHandler 自定义log的文件名 +# 原来 xxx.log xxx.log.1 xxx.log.2 xxx.log.3 文件由近及远 +# 现在 xxx.log xxx1.log xxx2.log 如果backupCount 是2位数时 则 01 02 03 三位数 001 002 .. 文件由近及远 +class RotatingFileHandler(BaseRotatingHandler): + def __init__( + self, filename, mode="a", maxBytes=0, backupCount=0, encoding=None, delay=0 + ): + # if maxBytes > 0: + # mode = 'a' + BaseRotatingHandler.__init__(self, filename, mode, encoding, delay) + self.maxBytes = maxBytes + self.backupCount = backupCount + self.placeholder = str(len(str(backupCount))) + + def doRollover(self): + if self.stream: + self.stream.close() + self.stream = None + if self.backupCount > 0: + for i in range(self.backupCount - 1, 0, -1): + sfn = ("%0" + self.placeholder + "d.") % i # '%2d.'%i -> 02 + sfn = sfn.join(self.baseFilename.split(".")) + # sfn = "%d_%s" % (i, self.baseFilename) + # dfn = "%d_%s" % (i + 1, self.baseFilename) + dfn = ("%0" + self.placeholder + "d.") % (i + 1) + dfn = dfn.join(self.baseFilename.split(".")) + if os.path.exists(sfn): + # print "%s -> %s" % (sfn, dfn) + if os.path.exists(dfn): + os.remove(dfn) + os.rename(sfn, dfn) + dfn = (("%0" + self.placeholder + "d.") % 1).join( + self.baseFilename.split(".") + ) + if os.path.exists(dfn): + os.remove(dfn) + # Issue 18940: A file may not have been created if delay is True. + if os.path.exists(self.baseFilename): + os.rename(self.baseFilename, dfn) + if not self.delay: + self.stream = self._open() + + def shouldRollover(self, record): + + if self.stream is None: # delay was set... + self.stream = self._open() + if self.maxBytes > 0: # are we rolling over? + msg = "%s\n" % self.format(record) + self.stream.seek(0, 2) # due to non-posix-compliant Windows feature + if self.stream.tell() + len(msg) >= self.maxBytes: + return 1 + return 0 + + +class MyStreamHandler(logging.StreamHandler): + + def emit(self, record): + if record.levelname in ["ERROR", "CRITICAL"]: + record.colorName = "\033[0;31m " + record.colorNameSuffix = " \033[0m" + else: + record.colorName = "\033[0;34m " + record.colorNameSuffix = " \033[0m" + super().emit(record) + + +def get_logger( + name, path="", log_level="DEBUG", is_write_to_file=False, is_write_to_stdout=True +): + """ + @summary: 获取log + --------- + @param name: log名 + @param path: log文件存储路径 如 D://xxx.log + @param log_level: log等级 CRITICAL/ERROR/WARNING/INFO/DEBUG + @param is_write_to_file: 是否写入到文件 默认否 + --------- + @result: + """ + name = name.split(os.sep)[-1].split(".")[0] # 取文件名 + + logger = logging.getLogger(name) + logger.setLevel(log_level) + # %(funcName)s + formatter = logging.Formatter(LOG_FORMAT) + console_formatter = logging.Formatter(CONSOLE_LOG_FORMAT) + if PRINT_EXCEPTION_DETAILS: + formatter.formatException = lambda exc_info: format_exception(*exc_info) + + # 定义一个RotatingFileHandler,最多备份5个日志文件,每个日志文件最大10M + if is_write_to_file: + if path and not os.path.exists(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + + rf_handler = RotatingFileHandler( + path, mode="w", maxBytes=10 * 1024 * 1024, backupCount=20, encoding="utf8" + ) + rf_handler.setFormatter(formatter) + logger.addHandler(rf_handler) + if is_write_to_stdout: + stream_handler = MyStreamHandler() + logger._console = stream_handler + stream_handler.stream = sys.stdout + stream_handler.setFormatter(console_formatter) + logger.addHandler(stream_handler) + + _handler_list = [] + _handler_name_list = [] + # 检查是否存在重复handler + for _handler in logger.handlers: + if str(_handler) not in _handler_name_list: + _handler_name_list.append(str(_handler)) + _handler_list.append(_handler) + logger.handlers = _handler_list + return logger + + +# logging.disable(logging.DEBUG) # 关闭所有log + +# 不让打印log的配置 +STOP_LOGS = [ + # ES + "urllib3.response", + "urllib3.connection", + "elasticsearch.trace", + "requests.packages.urllib3.util", + "requests.packages.urllib3.util.retry", + "urllib3.util", + "requests.packages.urllib3.response", + "requests.packages.urllib3.contrib.pyopenssl", + "requests.packages", + "urllib3.util.retry", + "requests.packages.urllib3.contrib", + "requests.packages.urllib3.connectionpool", + "requests.packages.urllib3.poolmanager", + "urllib3.connectionpool", + "requests.packages.urllib3.connection", + "elasticsearch", + "log_request_fail", + # requests + "requests", + "selenium.webdriver.remote.remote_connection", + "selenium.webdriver.remote", + "selenium.webdriver", + "selenium", + # markdown + "MARKDOWN", + "build_extension", + # newspaper + "calculate_area", + "largest_image_url", + "newspaper.images", + "newspaper", + "Importing", + "PIL", +] + +# 关闭日志打印 +for STOP_LOG in STOP_LOGS: + log_level = eval("logging." + gloable_setting_dict.get("log_level").upper()) + logging.getLogger(STOP_LOG).setLevel(log_level) + +# print(logging.Logger.manager.loggerDict) # 取使用debug模块的name + +# 日志级别大小关系为:critical > error > warning > info > debug +_log = get_logger( + name=gloable_setting_dict.get("log_name"), + path=gloable_setting_dict.get("log_path"), + log_level=gloable_setting_dict.get("log_level").upper(), + is_write_to_file=gloable_setting_dict.get("is_write_to_file"), +) + +log = _log diff --git a/smart/middlewire.py b/smart/middlewire.py new file mode 100644 index 0000000..7996287 --- /dev/null +++ b/smart/middlewire.py @@ -0,0 +1,77 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: middlewire +# Author: liangbaikai +# Date: 2020/12/28 +# Desc: there is a python file description +# ------------------------------------------------------------------ +from copy import copy +from functools import wraps +from typing import Union, Callable + + +class Middleware: + def __init__(self): + # request middleware + self.request_middleware = [] + # response middleware + self.response_middleware = [] + + def request(self, order_or_func: Union[int, Callable]): + def outWrap(func): + """ + Define a Decorate to be called before a request. + eg: @middleware.request + """ + middleware = func + + @wraps(func) + def register_middleware(*args, **kwargs): + self.request_middleware.append((order_or_func, middleware)) + self.request_middleware = sorted(self.request_middleware, key=lambda key: key[0]) + return middleware + + return register_middleware() + + if callable(order_or_func): + cp_order = copy(order_or_func) + order_or_func = 0 + return outWrap(cp_order) + return outWrap + + def response(self, order_or_func: Union[int, Callable]): + def outWrap(func): + """ + Define a Decorate to be called before a request. + eg: @middleware.request + """ + middleware = func + + @wraps(middleware) + def register_middleware(*args, **kwargs): + self.response_middleware.append((order_or_func, middleware)) + self.response_middleware = sorted(self.response_middleware, key=lambda key: key[0], reverse=True) + return middleware + + return register_middleware() + + if callable(order_or_func): + cp_order = copy(order_or_func) + order_or_func = 0 + return outWrap(cp_order) + return outWrap + + def __add__(self, other): + new_middleware = Middleware() + # asc + new_middleware.request_middleware.extend(self.request_middleware) + new_middleware.request_middleware.extend(other.request_middleware) + new_middleware.request_middleware = sorted(new_middleware.request_middleware, key=lambda key: key[0]) + + # desc + new_middleware.response_middleware.extend(other.response_middleware) + new_middleware.response_middleware.extend(self.response_middleware) + new_middleware.response_middleware = sorted(new_middleware.response_middleware, + key=lambda key: key[0], + reverse=True) + return new_middleware diff --git a/smart/pipline.py b/smart/pipline.py new file mode 100644 index 0000000..b5ba3e7 --- /dev/null +++ b/smart/pipline.py @@ -0,0 +1,44 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: middlewire +# Author: liangbaikai +# Date: 2020/12/28 +# Desc: there is a python file description +# ------------------------------------------------------------------ +from copy import copy +from functools import wraps +from typing import Union, Callable + + +class Piplines: + def __init__(self): + # item piplines + self.piplines = [] + + def pipline(self, order_or_func: Union[int, Callable]): + def outWrap(func): + """ + Define a Decorate to be called before a request. + eg: @middleware.request + """ + + @wraps(func) + def register_pip(*args, **kwargs): + self.piplines.append((order_or_func, func)) + self.piplines = sorted(self.piplines, key=lambda key: key[0]) + return func + + return register_pip() + + if callable(order_or_func): + cp_order = copy(order_or_func) + order_or_func = 0 + return outWrap(cp_order) + return outWrap + + def __add__(self, other): + pls = Piplines() + pls.piplines.extend(self.piplines) + pls.piplines.extend(other.piplines) + pls.piplines = sorted(pls.piplines, key=lambda key: key[0]) + return pls diff --git a/smart/request.py b/smart/request.py new file mode 100644 index 0000000..0c0a1a6 --- /dev/null +++ b/smart/request.py @@ -0,0 +1,51 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: request +# Author: liangbaikai +# Date: 2020/12/21 +# Desc: there is a python file description +# ------------------------------------------------------------------ +from dataclasses import dataclass, field, InitVar +from typing import Callable + +from smart.tool import is_valid_url + + +@dataclass +class Request: + url: InitVar[str] + callback: Callable = None + method: str = 'get' + timeout: float = None + # if None will auto detect encoding + encoding: str = None + header: dict = None + cookies: dict = None + # post data + data: any = None + # http requets kwargs.. + extras: dict = None + # different callback functions can be delivered + meta: dict = None + # do not filter repeat url + dont_filter: bool = False + # no more than max retry times and retry is delay retry + _retry: int = 0 + + def __post_init__(self, url): + if is_valid_url(url): + self.url = url + else: + raise ValueError( + f"request url [{url}] is not a valid url,does it hava a schemas, url is valid like http://www.baidu.com") + + @property + def retry(self): + return self._retry + + @retry.setter + def retry(self, value): + if isinstance(value, int): + self._retry = value + else: + raise ValueError("need a int value") diff --git a/smart/response.py b/smart/response.py new file mode 100644 index 0000000..bd5a468 --- /dev/null +++ b/smart/response.py @@ -0,0 +1,110 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: response +# Author: liangbaikai +# Date: 2020/12/21 +# Desc: there is a python file description +# ------------------------------------------------------------------ +import json +from dataclasses import dataclass +from typing import List, Dict, Union, Any, Optional + +import cchardet +from jsonpath import jsonpath +from parsel import Selector, SelectorList + +from smart.tool import get_index_url +from .request import Request + + +@dataclass +class Response: + body: bytes + status: int + request: Request = None + headers: dict = None + cookies: dict = None + _selector: Selector = None + + def xpath(self, xpath_str) -> Union[SelectorList]: + """ + 个别html可能不兼容 导致无法搜索到结果 + :param xpath_str: xpath str + :return: SelectorList + """ + return self.selector.xpath(xpath_str) + + def css(self, css_str) -> Union[SelectorList]: + return self.selector.css(css_str) + + def re(self, partern, replace_entities=True) -> List: + return self.selector.re(partern, replace_entities) + + def re_first(self, partern, default=None, replace_entities=True) -> Any: + return self.selector.re_first(partern, default, replace_entities) + + def json(self) -> Dict: + return json.loads(self.text) + + def jsonpath(self, jsonpath_str) -> List: + res = jsonpath(self.json(), jsonpath_str) + return [] if isinstance(res, bool) and not res else res + + def get_base_url(self) -> str: + return get_index_url(self.url) + + def urljoin(self, url) -> str: + if url is None or url == '': + raise ValueError("urljoin called, the url can not be empty") + schema_suffix = "http" + if url.startswith("%s" % schema_suffix): + return url + else: + basr_url = self.get_base_url() + return basr_url + url if url.startswith("/") else basr_url + "/" + url + + def links(self) -> List[str]: + xpath = self.selector.xpath("//@href") + full_urls = [] + for _item in xpath: + link = _item.get() + if link and "javascript:" not in link and len(link) > 1: + if self.request: + link = self.urljoin(link) + full_urls.append(link) + return full_urls + + @property + def selector(self) -> Selector: + if not self._selector: + self._selector = Selector(self.text) + return self._selector + + @property + def content(self) -> bytes: + return self.body + + @property + def text(self) -> Optional[str]: + if not self.body: + return None + # if request encoding is none and then auto detect encoding + self.request.encoding = self.encoding or cchardet.detect(self.body)["encoding"] + # minimum possible may be UnicodeDecodeError + return self.body.decode(self.encoding) + + @property + def url(self) -> str: + return self.request.url + + @property + def meta(self) -> Dict: + return self.request.meta + + @property + def encoding(self) -> str: + return self.request.encoding + + @property + def ok(self) -> bool: + return self.status == 0 or 200 <= self.status <= 299 diff --git a/smart/runer.py b/smart/runer.py new file mode 100644 index 0000000..b77c94b --- /dev/null +++ b/smart/runer.py @@ -0,0 +1,170 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: run +# Author: liangbaikai +# Date: 2020/12/22 +# Desc: there is a python file description +# ------------------------------------------------------------------ +import asyncio +import importlib +import inspect +import sys +import time +from asyncio import CancelledError +from concurrent.futures.thread import ThreadPoolExecutor +from typing import List +from urllib.request import urlopen + +from smart.log import log +from smart.core import Engine +from smart.middlewire import Middleware +from smart.pipline import Piplines +from smart.setting import gloable_setting_dict +from smart.spider import Spider +from smart.tool import is_valid_url + + +class CrawStater: + __version = "0.1.0" + + def __init__(self, loop=None): + if sys.platform == "win32": + # avoid a certain extent: too many files error + loop = loop or asyncio.ProactorEventLoop() + else: + # uvloop performance is better on linux.. + # todo use uvloop + self.loop = loop or asyncio.new_event_loop() + thread_pool_max_size = gloable_setting_dict.get( + "thread_pool_max_size", 30) + loop.set_default_executor(ThreadPoolExecutor(thread_pool_max_size)) + asyncio.set_event_loop(loop) + self.loop = loop + self.cores = [] + self.log = log + self.spider_names = [] + + def run_many(self, spiders: List[Spider], middlewire: Middleware = None, pipline: Piplines = None): + if not spiders or len(spiders) <= 0: + raise ValueError("need spiders") + for spider in spiders: + if not isinstance(spider, Spider): + raise ValueError("need a Spider sub instance") + _middle = spider.cutome_setting_dict.get("middleware_instance") or middlewire + _pip = spider.cutome_setting_dict.get("piplines_instance") or pipline + core = Engine(spider, _middle, _pip) + self.cores.append(core) + self.spider_names.append(spider.name) + self._check_internet_state() + self._run() + + def run_single(self, spider: Spider, middlewire: Middleware = None, pipline: Piplines = None): + if not spider: + raise ValueError("need a Spider class or Spider sub instance") + if not isinstance(spider, Spider): + raise ValueError("need a Spider sub instance") + _middle = spider.cutome_setting_dict.get("middleware_instance") or middlewire + _pip = spider.cutome_setting_dict.get("piplines_instance") or pipline + core = Engine(spider, _middle, _pip) + self.cores.append(core) + self.spider_names.append(spider.name) + self._run() + + def run(self, spider_module: str, spider_names: List[str] = [], middlewire: Middleware = None, + pipline: Piplines = None): + + spider_module = importlib.import_module(f'{spider_module}') + spider = [x for x in inspect.getmembers(spider_module, + predicate=lambda x: inspect.isclass(x)) if + issubclass(x[1], Spider) and x[1] != Spider] + if spider and len(spider) > 0: + for tuple_item in spider: + if (not spider_names or len(spider_names) <= 0) \ + or tuple_item[1].name in spider_names: + _middle = tuple_item[1].cutome_setting_dict.get("middleware_instance") or middlewire + _pip = tuple_item[1].cutome_setting_dict.get("piplines_instance") or pipline + _spider = tuple_item[1]() + if not isinstance(_spider, Spider): + raise ValueError("need a Spider sub instance") + core = Engine(_spider, _middle, _pip) + self.cores.append(core) + self.spider_names.append(_spider.name) + self._run() + + + def stop(self): + self.log.info(f'warning stop be called, {",".join(self.spider_names)} will stop ') + for core in self.cores: + self.loop.call_soon_threadsafe(core.close) + + def pause(self): + self.log.info(f'warning pause be called, {",".join(self.spider_names)} will pause ') + for core in self.cores: + self.loop.call_soon_threadsafe(core.pause) + + def recover(self): + self.log.info(f'warning recover be called, {",".join(self.spider_names)} will recover ') + for core in self.cores: + self.loop.call_soon_threadsafe(core.recover) + + def _run(self): + start = time.time() + tasks = [] + for core in self.cores: + self.log.info(f'{core.spider.name} start run..') + future = asyncio.ensure_future(core.start(), loop=self.loop) + tasks.append(future) + if len(tasks) <= 0: + raise ValueError("can not finded spider tasks to start so ended...") + self._print_info() + try: + group_tasks = asyncio.gather(*tasks, loop=self.loop) + self.loop.run_until_complete(group_tasks) + except CancelledError as e: + self.log.debug(f" in loop, occured CancelledError e {e} ", exc_info=True) + except KeyboardInterrupt as e2: + self.log.debug(f" in loop, occured KeyboardInterrupt e {e2} ") + self.stop() + except BaseException as e3: + self.log.error(f" in loop, occured BaseException e {e3} ", exc_info=True) + + self.log.info(f'craw succeed {",".join(self.spider_names)} ended.. it cost {round(time.time() - start,3)} s') + + + def _print_info(self): + self.log.info("good luck!") + self.log.info( + """ + _____ _ _____ _ _ + / ____| | | / ____| (_) | | + | (___ _ __ ___ __ _ _ __| |_ _____| (___ _ __ _ __| | ___ _ __ + \___ \| '_ ` _ \ / _` | '__| __|______\___ \| '_ \| |/ _` |/ _ \ '__| + ____) | | | | | | (_| | | | |_ ____) | |_) | | (_| | __/ | + |_____/|_| |_| |_|\__,_|_| \__| |_____/| .__/|_|\__,_|\___|_| + | | + |_| + + """ + ) + self.log.info(" \r\n smart-spider-framework" + f"\r\n os: {sys.platform}" + " \r\n author: liangbaikai" + " \r\n emial:1144388620@qq.com" + " \r\n version: 0.1.0" + " \r\n proverbs: whatever is worth doing is worth doing well." + ) + + @classmethod + def _check_internet_state(cls): + error_msg = "internet may not be available please check net, run ended" + net_healthy_check_url = gloable_setting_dict.get("net_healthy_check_url", None) + if net_healthy_check_url is None: + return + if not is_valid_url(net_healthy_check_url): + return + try: + resp = urlopen(url=net_healthy_check_url, timeout=10) + if not 200 <= resp.status <= 299: + raise RuntimeError(error_msg) + except Exception: + raise RuntimeError(error_msg) diff --git a/smart/scheduler.py b/smart/scheduler.py new file mode 100644 index 0000000..a52353a --- /dev/null +++ b/smart/scheduler.py @@ -0,0 +1,99 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: scheduler +# Author: liangbaikai +# Date: 2020/12/21 +# Desc: there is a python file description +# ------------------------------------------------------------------ +from collections import deque +from typing import Optional + +from smart.log import log +from smart.request import Request + +from abc import ABC, abstractmethod + + +class BaseSchedulerContainer(ABC): + + @abstractmethod + def push(self, request: Request): + pass + + @abstractmethod + def pop(self) -> Optional[Request]: + pass + + +class BaseDuplicateFilter(ABC): + + @abstractmethod + def add(self, url): + pass + + @abstractmethod + def contains(self, url): + pass + + @abstractmethod + def length(self): + pass + + +class SampleDuplicateFilter(BaseDuplicateFilter): + + def __init__(self): + self.set_container = set() + + def add(self, url): + if url: + self.set_container.add(hash(url)) + + def contains(self, url): + if not url: + return False + if hash(url) in self.set_container: + return True + return False + + def length(self): + return len(self.set_container) + + +class DequeSchedulerContainer(BaseSchedulerContainer): + def __init__(self): + self.url_queue = deque() + + def push(self, request: Request): + self.url_queue.append(request) + + def pop(self) -> Optional[Request]: + if self.url_queue: + return self.url_queue.popleft() + return None + + +class Scheduler: + def __init__(self, duplicate_filter: BaseDuplicateFilter = None, + scheduler_container: BaseSchedulerContainer = None): + if duplicate_filter is None: + duplicate_filter = SampleDuplicateFilter() + if scheduler_container is None: + scheduler_container = DequeSchedulerContainer() + self.scheduler_container = scheduler_container + self.duplicate_filter = duplicate_filter + self.log = log + + def schedlue(self, request: Request): + self.log.debug(f"get a request {request} wating toschedlue ") + if not request.dont_filter: + _url = request.url + ":" + str(request.retry) + if self.duplicate_filter.contains(_url): + self.log.debug(f"duplicate_filter filted ... url{_url} ") + return + self.duplicate_filter.add(_url) + self.scheduler_container.push(request) + + def get(self) -> Optional[Request]: + self.log.debug(f"get a request to download task ") + return self.scheduler_container.pop() diff --git a/smart/setting.py b/smart/setting.py new file mode 100644 index 0000000..15f0af3 --- /dev/null +++ b/smart/setting.py @@ -0,0 +1,42 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: setting +# Author: liangbaikai +# Date: 2021/1/4 +# Desc: there is a python file description +# ------------------------------------------------------------------ + +gloable_setting_dict = { + # 请求延迟 + "req_delay": 0, + # request timeout 10 s + "req_timeout": 10, + # 每个爬虫的请求并发数 + "req_per_concurrent": 100, + # 每个请求的最大重试次数 + "req_max_retry": 3, + # 默认请求头 + "default_headers": { + "user-agent": "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" + }, + # 请求url 去重处理器 + # 自己实现需要继承 BaseDuplicateFilter 实现相关抽象方法 系统默认SampleDuplicateFilter + "duplicate_filter_class": "smart.scheduler.SampleDuplicateFilter", + # 请求url调度器容器 + # 自己实现需要继承 BaseSchedulerContainer 实现相关抽象方法 系统默认DequeSchedulerContainer + "scheduler_container_class": "smart.scheduler.DequeSchedulerContainer", + # 请求网络的方法 输入 request 输出 response + # 自己实现需要继承 BaseDown 实现相关抽象方法 系统默认AioHttpDown + "net_download_class": "smart.downloader.AioHttpDown", + # 线程池数 当 middwire pipline 有不少耗时的同步方法时 适当调大 + "thread_pool_max_size": 50, + # 根据响应的状态码 忽略以下响应 + "ignore_response_codes": [401, 403, 404, 405, 500, 502, 504], + # 网络是否畅通检查地址 + "net_healthy_check_url": "https://www.baidu.com", + # log level + "log_level": "info", + "log_name": "smart-spider", + "log_path": "D://test//smart.log", + "is_write_to_file": False, +} diff --git a/smart/spider.py b/smart/spider.py new file mode 100644 index 0000000..e7fcd30 --- /dev/null +++ b/smart/spider.py @@ -0,0 +1,69 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: spider +# Author: liangbaikai +# Date: 2020/12/21 +# Desc: there is a abstract spider +# ------------------------------------------------------------------ +from abc import ABC, abstractmethod +import uuid +from typing import List + +from smart.request import Request +from smart.response import Response + + +class SpiderHook(ABC): + + def on_start(self): + pass + + def on_close(self): + pass + + def on_exception_occured(self, e: Exception): + pass + + +class Spider(SpiderHook): + name: str = f'smart-spider-{uuid.uuid4()}' + + # spider leaf state: init | runing | paused | closed + state: str = "init" + + start_urls: List[str] = [] + + cutome_setting_dict = { + # 请求延迟 + "req_delay": None, + # 每个爬虫的请求并发数 + "req_per_concurrent": None, + # 每个请求的最大重试次数 + "req_max_try": None, + # 默认请求头 + "default_headers": None, + # 根据响应的状态码 忽略以下响应 + "ignore_response_codes": None, + "middleware_instance": None, + "piplines_instance": None, + # 请求url 去重处理器 + # 自己实现需要继承 BaseDuplicateFilter 实现相关抽象方法 系统默认SampleDuplicateFilter + "duplicate_filter_class": "smart.scheduler.SampleDuplicateFilter", + # 请求url调度器容器 + # 自己实现需要继承 BaseSchedulerContainer 实现相关抽象方法 系统默认DequeSchedulerContainer + "scheduler_container_class": "smart.scheduler.DequeSchedulerContainer", + # 请求网络的方法 输入 request 输出 response + # 自己实现需要继承 BaseDown 实现相关抽象方法 系统默认AioHttpDown + "net_download_class": "smart.downloader.AioHttpDown", + } + + def start_requests(self): + for url in self.start_urls: + yield Request(url=url, callback=self.parse) + + @abstractmethod + def parse(self, response: Response): + ... + + def __iter__(self): + yield from self.start_requests() diff --git a/smart/tool.py b/smart/tool.py new file mode 100644 index 0000000..b56dc90 --- /dev/null +++ b/smart/tool.py @@ -0,0 +1,2195 @@ +# -*- coding: utf-8 -*- +""" +Created on 2018-09-06 14:21 +--------- +@summary: 工具 +--------- +@author: Boris +@email: boris@bzkj.tech +""" +import calendar +import codecs +import configparser # 读配置文件的 +import datetime +import functools +import hashlib +import html +import json +import os +import pickle +import random +import re +import socket +import ssl +import string +import sys +import time +import traceback +import urllib +import urllib.parse +import uuid +from hashlib import md5 +from pprint import pformat +from pprint import pprint +from urllib import request +from urllib.parse import urljoin + +# import execjs # pip install PyExecJS +# import redis +import requests +import six +from requests.cookies import RequestsCookieJar +from w3lib.url import canonicalize_url as sort_url + +# import spider.setting as setting +from smart.log import log + +os.environ["EXECJS_RUNTIME"] = "Node" # 设置使用node执行js + +# 全局取消ssl证书验证 +ssl._create_default_https_context = ssl._create_unverified_context + +TIME_OUT = 30 +TIMER_TIME = 5 + +redisdb = None + +CAMELCASE_INVALID_CHARS = re.compile(r'[^a-zA-Z\d]') + + +# def get_redisdb(): +# global redisdb +# if not redisdb: +# ip, port = setting.REDISDB_IP_PORTS.split(":") +# redisdb = redis.Redis( +# host=ip, +# port=port, +# db=setting.REDISDB_DB, +# password=setting.REDISDB_USER_PASS, +# decode_responses=True, +# ) # redis默认端口是6379 +# return redisdb + + +# 装饰器 +def log_function_time(func): + try: + + @functools.wraps(func) # 将函数的原来属性付给新函数 + def calculate_time(*args, **kw): + began_time = time.time() + callfunc = func(*args, **kw) + end_time = time.time() + log.debug(func.__name__ + " run time = " + str(end_time - began_time)) + return callfunc + + return calculate_time + except: + log.debug("求取时间无效 因为函数参数不符") + return func + + +def run_safe_model(module_name): + def inner_run_safe_model(func): + try: + + @functools.wraps(func) # 将函数的原来属性付给新函数 + def run_func(*args, **kw): + callfunc = None + try: + callfunc = func(*args, **kw) + except Exception as e: + log.error(module_name + ": " + func.__name__ + " - " + str(e)) + traceback.print_exc() + return callfunc + + return run_func + except Exception as e: + log.error(module_name + ": " + func.__name__ + " - " + str(e)) + traceback.print_exc() + return func + + return inner_run_safe_model + + +########################【网页解析相关】############################### + + +# @log_function_time +def get_html_by_requests( + url, headers=None, code="utf-8", data=None, proxies={}, with_response=False +): + html = "" + r = None + try: + if data: + r = requests.post( + url, headers=headers, timeout=TIME_OUT, data=data, proxies=proxies + ) + else: + r = requests.get(url, headers=headers, timeout=TIME_OUT, proxies=proxies) + + if code: + r.encoding = code + html = r.text + + except Exception as e: + log.error(e) + finally: + r and r.close() + + if with_response: + return html, r + else: + return html + + +def get_json_by_requests( + url, + params=None, + headers=None, + data=None, + proxies={}, + with_response=False, + cookies=None, +): + json = {} + response = None + try: + # response = requests.get(url, params = params) + if data: + response = requests.post( + url, + headers=headers, + data=data, + params=params, + timeout=TIME_OUT, + proxies=proxies, + cookies=cookies, + ) + else: + response = requests.get( + url, + headers=headers, + params=params, + timeout=TIME_OUT, + proxies=proxies, + cookies=cookies, + ) + response.encoding = "utf-8" + json = response.json() + except Exception as e: + log.error(e) + finally: + response and response.close() + + if with_response: + return json, response + else: + return json + + +def get_cookies(response): + cookies = requests.utils.dict_from_cookiejar(response.cookies) + return cookies + + +def get_cookies_jar(cookies): + """ + @summary: 适用于selenium生成的cookies转requests的cookies + requests.get(xxx, cookies=jar) + 参考:https://www.cnblogs.com/small-bud/p/9064674.html + + --------- + @param cookies: [{},{}] + --------- + @result: cookie jar + """ + + cookie_jar = RequestsCookieJar() + for cookie in cookies: + cookie_jar.set(cookie["name"], cookie["value"]) + + return cookie_jar + + +def get_cookies_from_selenium_cookie(cookies): + """ + @summary: 适用于selenium生成的cookies转requests的cookies + requests.get(xxx, cookies=jar) + 参考:https://www.cnblogs.com/small-bud/p/9064674.html + + --------- + @param cookies: [{},{}] + --------- + @result: cookie jar + """ + + cookie_dict = {} + for cookie in cookies: + if cookie.get("name"): + cookie_dict[cookie["name"]] = cookie["value"] + + return cookie_dict + + +def cookiesjar2str(cookies): + str_cookie = "" + for k, v in requests.utils.dict_from_cookiejar(cookies).items(): + str_cookie += k + str_cookie += "=" + str_cookie += v + str_cookie += "; " + return str_cookie + + +def cookies2str(cookies): + str_cookie = "" + for k, v in cookies.items(): + str_cookie += k + str_cookie += "=" + str_cookie += v + str_cookie += "; " + return str_cookie + + +def get_urls( + html, + stop_urls=( + "javascript", + "+", + ".css", + ".js", + ".rar", + ".xls", + ".exe", + ".apk", + ".doc", + ".jpg", + ".png", + ".flv", + ".mp4", + ), +): + # 不匹配javascript、 +、 # 这样的url + regex = r'>> string_camelcase('lost-pound') + 'LostPound' + + >>> string_camelcase('missing_images') + 'MissingImages' + + """ + return CAMELCASE_INVALID_CHARS.sub('', string.title()) + + +def get_full_url(root_url, sub_url): + """ + @summary: 得到完整的ur + --------- + @param root_url: 根url (网页的url) + @param sub_url: 子url (带有相对路径的 可以拼接成完整的) + --------- + @result: 返回完整的url + """ + + return urljoin(root_url, sub_url) + + +def joint_url(url, params): + # param_str = "?" + # for key, value in params.items(): + # value = isinstance(value, str) and value or str(value) + # param_str += key + "=" + value + "&" + # + # return url + param_str[:-1] + + if not params: + return url + + params = urlencode(params) + separator = "?" if "?" not in url else "&" + return url + separator + params + + +def canonicalize_url(url): + """ + url 归一化 会参数排序 及去掉锚点 + """ + return sort_url(url) + + +def get_url_md5(url): + url = canonicalize_url(url) + url = re.sub("^http://", "https://", url) + return get_md5(url) + + +def fit_url(urls, identis): + identis = isinstance(identis, str) and [identis] or identis + fit_urls = [] + for link in urls: + for identi in identis: + if identi in link: + fit_urls.append(link) + return list(set(fit_urls)) + + +def get_param(url, key): + params = url.split("?")[-1].split("&") + for param in params: + key_value = param.split("=", 1) + if key == key_value[0]: + return key_value[1] + return None + + +def urlencode(params): + """ + 字典类型的参数转为字符串 + @param params: + { + 'a': 1, + 'b': 2 + } + @return: a=1&b=2 + """ + return urllib.parse.urlencode(params) + + +def urldecode(url): + """ + 将字符串类型的参数转为json + @param url: xxx?a=1&b=2 + @return: + { + 'a': 1, + 'b': 2 + } + """ + params_json = {} + params = url.split("?")[-1].split("&") + for param in params: + key, value = param.split("=") + params_json[key] = unquote_url(value) + + return params_json + + +def unquote_url(url, encoding="utf-8"): + """ + @summary: 将url解码 + --------- + @param url: + --------- + @result: + """ + + return urllib.parse.unquote(url, encoding=encoding) + + +def quote_url(url, encoding="utf-8"): + """ + @summary: 将url编码 编码意思http://www.w3school.com.cn/tags/html_ref_urlencode.html + --------- + @param url: + --------- + @result: + """ + + return urllib.parse.quote(url, safe="%;/?:@&=+$,", encoding=encoding) + + +def quote_chinese_word(text, encoding="utf-8"): + def quote_chinese_word_func(text): + chinese_word = text.group(0) + return urllib.parse.quote(chinese_word, encoding=encoding) + + return re.sub("([\u4e00-\u9fa5]+)", quote_chinese_word_func, text, flags=re.S) + + +def unescape(str): + """ + 反转译 + """ + return html.unescape(str) + + +def excape(str): + """ + 转译 + """ + return html.escape(str) + + +_regexs = {} + + +# @log_function_time +def get_info(html, regexs, allow_repeat=True, fetch_one=False, split=None): + regexs = isinstance(regexs, str) and [regexs] or regexs + + infos = [] + for regex in regexs: + if regex == "": + continue + + if regex not in _regexs.keys(): + _regexs[regex] = re.compile(regex, re.S) + + if fetch_one: + infos = _regexs[regex].search(html) + if infos: + infos = infos.groups() + else: + continue + else: + infos = _regexs[regex].findall(str(html)) + + if len(infos) > 0: + # print(regex) + break + + if fetch_one: + infos = infos if infos else ("",) + return infos if len(infos) > 1 else infos[0] + else: + infos = allow_repeat and infos or sorted(set(infos), key=infos.index) + infos = split.join(infos) if split else infos + return infos + + +def table_json(table, save_one_blank=True): + """ + 将表格转为json 适应于 key:value 在一行类的表格 + @param table: 使用selector封装后的具有xpath的selector + @param save_one_blank: 保留一个空白符 + @return: + """ + data = {} + + trs = table.xpath(".//tr") + for tr in trs: + tds = tr.xpath("./td|./th") + + for i in range(0, len(tds), 2): + if i + 1 > len(tds) - 1: + break + + key = tds[i].xpath("string(.)").extract_first(default="").strip() + value = tds[i + 1].xpath("string(.)").extract_first(default="").strip() + value = replace_str(value, "[\f\n\r\t\v]", "") + value = replace_str(value, " +", " " if save_one_blank else "") + + if key: + data[key] = value + + return data + + +def get_table_row_data(table): + """ + 获取表格里每一行数据 + @param table: 使用selector封装后的具有xpath的selector + @return: [[],[]..] + """ + + datas = [] + rows = table.xpath(".//tr") + for row in rows: + cols = row.xpath("./td|./th") + row_datas = [] + for col in cols: + data = col.xpath("string(.)").extract_first(default="").strip() + row_datas.append(data) + datas.append(row_datas) + + return datas + + +def rows2json(rows, keys=None): + """ + 将行数据转为json + @param rows: 每一行的数据 + @param keys: json的key,空时将rows的第一行作为key + @return: + """ + data_start_pos = 0 if keys else 1 + datas = [] + keys = keys or rows[0] + for values in rows[data_start_pos:]: + datas.append(dict(zip(keys, values))) + + return datas + + +def get_form_data(form): + """ + 提取form中提交的数据 + :param form: 使用selector封装后的具有xpath的selector + :return: + """ + data = {} + inputs = form.xpath(".//input") + for input in inputs: + name = input.xpath("./@name").extract_first() + value = input.xpath("./@value").extract_first() + if name: + data[name] = value + + return data + + +# mac上不好使 +# def get_domain(url): +# domain = '' +# try: +# domain = get_tld(url) +# except Exception as e: +# log.debug(e) +# return domain + + +def get_domain(url): + proto, rest = urllib.parse.splittype(url) + domain, rest = urllib.parse.splithost(rest) + return domain + + +def get_index_url(url): + return "/".join(url.split("/")[:3]) + + +def get_ip(domain): + ip = socket.getaddrinfo(domain, "http")[0][4][0] + return ip + + +def get_localhost_ip(): + """ + 利用 UDP 协议来实现的,生成一个UDP包,把自己的 IP 放如到 UDP 协议头中,然后从UDP包中获取本机的IP。 + 这个方法并不会真实的向外部发包,所以用抓包工具是看不到的 + :return: + """ + s = None + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + finally: + if s: + s.close() + + return ip + + +def ip_to_num(ip): + import struct + + ip_num = socket.ntohl(struct.unpack("I", socket.inet_aton(str(ip)))[0]) + return ip_num + + +def is_valid_proxy(proxy, check_url=None): + """ + 检验代理是否有效 + @param proxy: xxx.xxx.xxx:xxx + @param check_url: 利用目标网站检查,目标网站url。默认为None, 使用代理服务器的socket检查, 但不能排除Connection closed by foreign host + @return: True / False + """ + is_valid = False + + if check_url: + proxies = {"http": f"http://{proxy}", "https": f"https://{proxy}"} + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36" + } + response = None + try: + response = requests.get( + check_url, headers=headers, proxies=proxies, stream=True, timeout=20 + ) + is_valid = True + + except Exception as e: + log.error("check proxy failed: {} {}".format(e, proxy)) + + finally: + if response: + response.close() + + else: + ip, port = proxy.split(":") + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sk: + sk.settimeout(7) + try: + sk.connect((ip, int(port))) # 检查代理服务器是否开着 + is_valid = True + + except Exception as e: + log.error("check proxy failed: {} {}:{}".format(e, ip, port)) + + return is_valid + + +def is_valid_url(url): + """ + 验证url是否合法 + :param url: + :return: + """ + if re.match(r"(^https?:/{2}\w.+$)|(ftp://)", url): + return True + else: + return False + + +def get_text(soup, *args): + try: + return soup.get_text() + except Exception as e: + log.error(e) + return "" + + +def del_html_tag(content, except_line_break=False, save_img=False, white_replaced=""): + """ + 删除html标签 + @param content: html内容 + @param except_line_break: 保留p标签 + @param save_img: 保留图片 + @param white_replaced: 空白符替换 + @return: + """ + content = replace_str(content, "(?i)") # (?)忽略大小写 + content = replace_str(content, "(?i)") + content = replace_str(content, "") + content = replace_str( + content, "(?!&[a-z]+=)&[a-z]+;?" + ) # 干掉 等无用的字符 但&xxx= 这种表示参数的除外 + if except_line_break: + content = content.replace("

", "/p") + content = replace_str(content, "<[^p].*?>") + content = content.replace("/p", "

") + content = replace_str(content, "[ \f\r\t\v]") + + elif save_img: + content = replace_str(content, "(?!)<.+?>") # 替换掉除图片外的其他标签 + content = replace_str(content, "(?! +)\s+", "\n") # 保留空格 + content = content.strip() + + else: + content = replace_str(content, "<(.|\n)*?>") + content = replace_str(content, "\s", white_replaced) + content = content.strip() + + return content + + +def del_html_js_css(content): + content = replace_str(content, "(?i)") # (?)忽略大小写 + content = replace_str(content, "(?i)") + content = replace_str(content, "") + + return content + + +def is_have_chinese(content): + regex = "[\u4e00-\u9fa5]+" + chinese_word = get_info(content, regex) + return chinese_word and True or False + + +def is_have_english(content): + regex = "[a-zA-Z]+" + english_words = get_info(content, regex) + return english_words and True or False + + +def get_chinese_word(content): + regex = "[\u4e00-\u9fa5]+" + chinese_word = get_info(content, regex) + return chinese_word + + +def get_english_words(content): + regex = "[a-zA-Z]+" + english_words = get_info(content, regex) + return english_words or "" + + +################################################## +def get_json(json_str): + """ + @summary: 取json对象 + --------- + @param json_str: json格式的字符串 + --------- + @result: 返回json对象 + """ + + try: + return json.loads(json_str) if json_str else {} + except Exception as e1: + try: + json_str = json_str.strip() + json_str = json_str.replace("'", '"') + keys = get_info(json_str, "(\w+):") + for key in keys: + json_str = json_str.replace(key, '"%s"' % key) + + return json.loads(json_str) if json_str else {} + + except Exception as e2: + log.error( + """ + e1: %s + format json_str: %s + e2: %s + """ + % (e1, json_str, e2) + ) + + return {} + + +def jsonp2json(jsonp): + """ + 将jsonp转为json + @param jsonp: jQuery172013600082560040794_1553230569815({}) + @return: + """ + try: + return json.loads(re.match(".*?({.*}).*", jsonp, re.S).group(1)) + except: + raise ValueError("Invalid Input") + + +def dumps_json(json_, indent=4, sort_keys=False): + """ + @summary: 格式化json 用于打印 + --------- + @param json_: json格式的字符串或json对象 + --------- + @result: 格式化后的字符串 + """ + try: + if isinstance(json_, str): + json_ = get_json(json_) + + json_ = json.dumps( + json_, ensure_ascii=False, indent=indent, skipkeys=True, sort_keys=sort_keys + ) + + except Exception as e: + log.error(e) + json_ = pformat(json_) + + return json_ + + +def get_json_value(json_object, key): + """ + @summary: + --------- + @param json_object: json对象或json格式的字符串 + @param key: 建值 如果在多个层级目录下 可写 key1.key2 如{'key1':{'key2':3}} + --------- + @result: 返回对应的值,如果没有,返回'' + """ + current_key = "" + value = "" + try: + json_object = ( + isinstance(json_object, str) and get_json(json_object) or json_object + ) + + current_key = key.split(".")[0] + value = json_object[current_key] + + key = key[key.find(".") + 1:] + except Exception as e: + return value + + if key == current_key: + return value + else: + return get_json_value(value, key) + + +def get_all_keys(datas, depth=None, current_depth=0): + """ + @summary: 获取json李所有的key + --------- + @param datas: dict / list + @param depth: 字典key的层级 默认不限制层级 层级从1开始 + @param current_depth: 字典key的当前层级 不用传参 + --------- + @result: 返回json所有的key + """ + + keys = [] + if depth and current_depth >= depth: + return keys + + if isinstance(datas, list): + for data in datas: + keys.extend(get_all_keys(data, depth, current_depth=current_depth + 1)) + elif isinstance(datas, dict): + for key, value in datas.items(): + keys.append(key) + if isinstance(value, dict): + keys.extend(get_all_keys(value, depth, current_depth=current_depth + 1)) + + return keys + + +def to_chinese(unicode_str): + format_str = json.loads('{"chinese":"%s"}' % unicode_str) + return format_str["chinese"] + + +################################################## +def replace_str(source_str, regex, replace_str=""): + """ + @summary: 替换字符串 + --------- + @param source_str: 原字符串 + @param regex: 正则 + @param replace_str: 用什么来替换 默认为'' + --------- + @result: 返回替换后的字符串 + """ + str_info = re.compile(regex) + return str_info.sub(replace_str, source_str) + + +def del_redundant_blank_character(text): + """ + 删除冗余的空白符, 只保留一个 + :param text: + :return: + """ + return re.sub("\s+", " ", text) + + +################################################## +def get_conf_value(config_file, section, key): + cp = configparser.ConfigParser(allow_no_value=True) + with codecs.open(config_file, "r", encoding="utf-8") as f: + cp.read_file(f) + return cp.get(section, key) + + +def mkdir(path): + try: + os.makedirs(path) + except OSError as exc: # Python >2.5 + pass + + +def write_file(filename, content, mode="w", encoding="utf-8"): + """ + @summary: 写文件 + --------- + @param filename: 文件名(有路径) + @param content: 内容 + @param mode: 模式 w/w+ (覆盖/追加) + --------- + @result: + """ + + directory = os.path.dirname(filename) + mkdir(directory) + with open(filename, mode, encoding=encoding) as file: + file.writelines(content) + + +def read_file(filename, readlines=False, encoding="utf-8"): + """ + @summary: 读文件 + --------- + @param filename: 文件名(有路径) + @param readlines: 按行读取 (默认False) + --------- + @result: 按行读取返回List,否则返回字符串 + """ + + content = None + try: + with open(filename, "r", encoding=encoding) as file: + content = file.readlines() if readlines else file.read() + except Exception as e: + log.error(e) + + return content + + +def get_oss_file_list(oss_handler, prefix, date_range_min, date_range_max=None): + """ + 获取文件列表 + @param prefix: 路径前缀 如 data/car_service_line/yiche/yiche_serial_zongshu_info + @param date_range_min: 时间范围 最小值 日期分隔符为/ 如 2019/03/01 或 2019/03/01/00/00/00 + @param date_range_max: 时间范围 最大值 日期分隔符为/ 如 2019/03/01 或 2019/03/01/00/00/00 + @return: 每个文件路径 如 html/e_commerce_service_line/alibaba/alibaba_shop_info/2019/03/22/15/53/15/8ca8b9e4-4c77-11e9-9dee-acde48001122.json.snappy + """ + + # 计算时间范围 + date_range_max = date_range_max or date_range_min + date_format = "/".join( + ["%Y", "%m", "%d", "%H", "%M", "%S"][: date_range_min.count("/") + 1] + ) + time_interval = [ + {"days": 365}, + {"days": 31}, + {"days": 1}, + {"hours": 1}, + {"minutes": 1}, + {"seconds": 1}, + ][date_range_min.count("/")] + date_range = get_between_date( + date_range_min, date_range_max, date_format=date_format, **time_interval + ) + + for date in date_range: + file_folder_path = os.path.join(prefix, date) + objs = oss_handler.list(prefix=file_folder_path) + for obj in objs: + filename = obj.key + yield filename + + +def is_html(url): + if not url: + return False + + try: + content_type = request.urlopen(url).info().get("Content-Type", "") + + if "text/html" in content_type: + return True + else: + return False + except Exception as e: + log.error(e) + return False + + +def is_exist(file_path): + """ + @summary: 文件是否存在 + --------- + @param file_path: + --------- + @result: + """ + + return os.path.exists(file_path) + + +def download_file(url, base_path, filename="", call_func="", proxies=None, data=None): + file_path = base_path + filename + directory = os.path.dirname(file_path) + mkdir(directory) + + # 进度条 + def progress_callfunc(blocknum, blocksize, totalsize): + """回调函数 + @blocknum : 已经下载的数据块 + @blocksize : 数据块的大小 + @totalsize: 远程文件的大小 + """ + percent = 100.0 * blocknum * blocksize / totalsize + if percent > 100: + percent = 100 + # print ('进度条 %.2f%%' % percent, end = '\r') + sys.stdout.write("进度条 %.2f%%" % percent + "\r") + sys.stdout.flush() + + if url: + try: + log.debug( + """ + 正在下载 %s + 存储路径 %s + """ + % (url, file_path) + ) + if proxies: + # create the object, assign it to a variable + proxy = request.ProxyHandler(proxies) + # construct a new opener using your proxy settings + opener = request.build_opener(proxy) + # install the openen on the module-level + request.install_opener(opener) + + request.urlretrieve(url, file_path, progress_callfunc, data) + + log.debug( + """ + 下载完毕 %s + 文件路径 %s + """ + % (url, file_path) + ) + + call_func and call_func() + return 1 + except Exception as e: + log.error(e) + return 0 + else: + return 0 + + +def get_file_list(path, ignore=[]): + templist = path.split("*") + path = templist[0] + file_type = templist[1] if len(templist) >= 2 else "" + + # 递归遍历文件 + def get_file_list_(path, file_type, ignore, all_file=[]): + file_list = os.listdir(path) + + for file_name in file_list: + if file_name in ignore: + continue + + file_path = os.path.join(path, file_name) + if os.path.isdir(file_path): + get_file_list_(file_path, file_type, ignore, all_file) + else: + if not file_type or file_name.endswith(file_type): + all_file.append(file_path) + + return all_file + + return get_file_list_(path, file_type, ignore) if os.path.isdir(path) else [path] + + +def rename_file(old_name, new_name): + os.rename(old_name, new_name) + + +def del_file(path, ignore=()): + files = get_file_list(path, ignore) + for file in files: + try: + os.remove(file) + except Exception as e: + log.error( + """ + 删除出错: %s + Exception : %s + """ + % (file, str(e)) + ) + finally: + pass + + +def get_file_type(file_name): + """ + @summary: 取文件后缀名 + --------- + @param file_name: + --------- + @result: + """ + try: + return os.path.splitext(file_name)[1] + except Exception as e: + log.exception(e) + + +def get_file_path(file_path): + """ + @summary: 取文件路径 + --------- + @param file_path: /root/a.py + --------- + @result: /root + """ + try: + return os.path.split(file_path)[0] + except Exception as e: + log.exception(e) + + +############################################# + +# +# def exec_js(js_code): +# """ +# @summary: 执行js代码 +# --------- +# @param js_code: js代码 +# --------- +# @result: 返回执行结果 +# """ +# +# return execjs.eval(js_code) + + +# def compile_js(js_func): +# """ +# @summary: 编译js函数 +# --------- +# @param js_func:js函数 +# --------- +# @result: 返回函数对象 调用 fun('js_funName', param1,param2) +# """ +# +# ctx = execjs.compile(js_func) +# return ctx.call + + +############################################### + +############################################# + + +def date_to_timestamp(date, time_format="%Y-%m-%d %H:%M:%S"): + """ + @summary: + --------- + @param date:将"2011-09-28 10:00:00"时间格式转化为时间戳 + @param format:时间格式 + --------- + @result: 返回时间戳 + """ + + timestamp = time.mktime(time.strptime(date, time_format)) + return int(timestamp) + + +def timestamp_to_date(timestamp, time_format="%Y-%m-%d %H:%M:%S"): + """ + @summary: + --------- + @param timestamp: 将时间戳转化为日期 + @param format: 日期格式 + --------- + @result: 返回日期 + """ + if timestamp is None: + raise ValueError("timestamp is null") + + date = time.localtime(timestamp) + return time.strftime(time_format, date) + + +def get_current_timestamp(): + return int(time.time()) + + +def get_current_date(date_format="%Y-%m-%d %H:%M:%S"): + return datetime.datetime.now().strftime(date_format) + # return time.strftime(date_format, time.localtime(time.time())) + + +def get_date_number(year=None, month=None, day=None): + """ + @summary: 获取指定日期对应的日期数 + 默认当前周 + --------- + @param year: 2010 + @param month: 6 + @param day: 16 + --------- + @result: (年号,第几周,第几天) 如 (2010, 24, 3) + """ + if year and month and day: + return datetime.date(year, month, day).isocalendar() + elif not any([year, month, day]): + return datetime.datetime.now().isocalendar() + else: + assert year, "year 不能为空" + assert month, "month 不能为空" + assert day, "day 不能为空" + + +def get_between_date( + begin_date, end_date=None, date_format="%Y-%m-%d", **time_interval +): + """ + @summary: 获取一段时间间隔内的日期,默认为每一天 + --------- + @param begin_date: 开始日期 str 如 2018-10-01 + @param end_date: 默认为今日 + @param date_format: 日期格式,应与begin_date的日期格式相对应 + @param time_interval: 时间间隔 默认一天 支持 days、seconds、microseconds、milliseconds、minutes、hours、weeks + --------- + @result: list 值为字符串 + """ + + date_list = [] + + begin_date = datetime.datetime.strptime(begin_date, date_format) + end_date = ( + datetime.datetime.strptime(end_date, date_format) + if end_date + else datetime.datetime.strptime( + time.strftime(date_format, time.localtime(time.time())), date_format + ) + ) + time_interval = time_interval or dict(days=1) + + while begin_date <= end_date: + date_str = begin_date.strftime(date_format) + date_list.append(date_str) + + begin_date += datetime.timedelta(**time_interval) + + if end_date.strftime(date_format) not in date_list: + date_list.append(end_date.strftime(date_format)) + + return date_list + + +def get_between_months(begin_date, end_date=None): + """ + @summary: 获取一段时间间隔内的月份 + 需要满一整月 + --------- + @param begin_date: 开始时间 如 2018-01-01 + @param end_date: 默认当前时间 + --------- + @result: 列表 如 ['2018-01', '2018-02'] + """ + + def add_months(dt, months): + month = dt.month - 1 + months + year = dt.year + month // 12 + month = month % 12 + 1 + day = min(dt.day, calendar.monthrange(year, month)[1]) + return dt.replace(year=year, month=month, day=day) + + date_list = [] + begin_date = datetime.datetime.strptime(begin_date, "%Y-%m-%d") + end_date = ( + datetime.datetime.strptime(end_date, "%Y-%m-%d") + if end_date + else datetime.datetime.strptime( + time.strftime("%Y-%m-%d", time.localtime(time.time())), "%Y-%m-%d" + ) + ) + while begin_date <= end_date: + date_str = begin_date.strftime("%Y-%m") + date_list.append(date_str) + begin_date = add_months(begin_date, 1) + return date_list + + +def get_today_of_day(day_offset=0): + return str(datetime.date.today() + datetime.timedelta(days=day_offset)) + + +def get_days_of_month(year, month): + """ + 返回天数 + """ + + return calendar.monthrange(year, month)[1] + + +def get_firstday_of_month(date): + """'' + date format = "YYYY-MM-DD" + """ + + year, month, day = date.split("-") + year, month, day = int(year), int(month), int(day) + + days = "01" + if int(month) < 10: + month = "0" + str(int(month)) + arr = (year, month, days) + return "-".join("%s" % i for i in arr) + + +def get_lastday_of_month(date): + """'' + get the last day of month + date format = "YYYY-MM-DD" + """ + year, month, day = date.split("-") + year, month, day = int(year), int(month), int(day) + + days = calendar.monthrange(year, month)[1] + month = add_zero(month) + arr = (year, month, days) + return "-".join("%s" % i for i in arr) + + +def get_firstday_month(month_offset=0): + """'' + get the first day of month from today + month_offset is how many months + """ + (y, m, d) = get_year_month_and_days(month_offset) + d = "01" + arr = (y, m, d) + return "-".join("%s" % i for i in arr) + + +def get_lastday_month(month_offset=0): + """'' + get the last day of month from today + month_offset is how many months + """ + return "-".join("%s" % i for i in get_year_month_and_days(month_offset)) + + +def get_last_month(month_offset=0): + """'' + get the last day of month from today + month_offset is how many months + """ + return "-".join("%s" % i for i in get_year_month_and_days(month_offset)[:2]) + + +def get_year_month_and_days(month_offset=0): + """ + @summary: + --------- + @param month_offset: 月份偏移量 + --------- + @result: ('2019', '04', '30') + """ + + today = datetime.datetime.now() + year, month = today.year, today.month + + this_year = int(year) + this_month = int(month) + total_month = this_month + month_offset + if month_offset >= 0: + if total_month <= 12: + days = str(get_days_of_month(this_year, total_month)) + total_month = add_zero(total_month) + return (year, total_month, days) + else: + i = total_month // 12 + j = total_month % 12 + if j == 0: + i -= 1 + j = 12 + this_year += i + days = str(get_days_of_month(this_year, j)) + j = add_zero(j) + return (str(this_year), str(j), days) + else: + if (total_month > 0) and (total_month < 12): + days = str(get_days_of_month(this_year, total_month)) + total_month = add_zero(total_month) + return (year, total_month, days) + else: + i = total_month // 12 + j = total_month % 12 + if j == 0: + i -= 1 + j = 12 + this_year += i + days = str(get_days_of_month(this_year, j)) + j = add_zero(j) + return (str(this_year), str(j), days) + + +def add_zero(n): + return "%02d" % n + + +def get_month(month_offset=0): + """'' + 获取当前日期前后N月的日期 + if month_offset>0, 获取当前日期前N月的日期 + if month_offset<0, 获取当前日期后N月的日期 + date format = "YYYY-MM-DD" + """ + today = datetime.datetime.now() + day = add_zero(today.day) + + (y, m, d) = get_year_month_and_days(month_offset) + arr = (y, m, d) + if int(day) < int(d): + arr = (y, m, day) + return "-".join("%s" % i for i in arr) + + +@run_safe_model("format_date") +def format_date(date, old_format="", new_format="%Y-%m-%d %H:%M:%S"): + """ + @summary: 格式化日期格式 + --------- + @param date: 日期 eg:2017年4月17日 3时27分12秒 + @param old_format: 原来的日期格式 如 '%Y年%m月%d日 %H时%M分%S秒' + %y 两位数的年份表示(00-99) + %Y 四位数的年份表示(000-9999) + %m 月份(01-12) + %d 月内中的一天(0-31) + %H 24小时制小时数(0-23) + %I 12小时制小时数(01-12) + %M 分钟数(00-59) + %S 秒(00-59) + @param new_format: 输出的日期格式 + --------- + @result: 格式化后的日期,类型为字符串 如2017-4-17 3:27:12 + """ + if not date: + return "" + + if not old_format: + regex = "(\d+)" + numbers = get_info(date, regex, allow_repeat=True) + formats = ["%Y", "%m", "%d", "%H", "%M", "%S"] + old_format = date + for i, number in enumerate(numbers[:6]): + if i == 0 and len(number) == 2: # 年份可能是两位 用小%y + old_format = old_format.replace( + number, formats[i].lower(), 1 + ) # 替换一次 '2017年11月30日 11:49' 防止替换11月时,替换11小时 + else: + old_format = old_format.replace(number, formats[i], 1) # 替换一次 + + try: + date_obj = datetime.datetime.strptime(date, old_format) + if "T" in date and "Z" in date: + date_obj += datetime.timedelta(hours=8) + date_str = date_obj.strftime("%Y-%m-%d %H:%M:%S") + else: + date_str = datetime.datetime.strftime(date_obj, new_format) + + except Exception as e: + log.error("日期格式化出错,old_format = %s 不符合 %s 格式" % (old_format, date)) + date_str = date + + return date_str + + +@run_safe_model("format_time") +def format_time(release_time, date_format="%Y-%m-%d %H:%M:%S"): + if "年前" in release_time: + years = re.compile("(\d+)年前").findall(release_time) + years_ago = datetime.datetime.now() - datetime.timedelta( + days=int(years[0]) * 365 + ) + release_time = years_ago.strftime("%Y-%m-%d %H:%M:%S") + + elif "月前" in release_time: + months = re.compile("(\d+)月前").findall(release_time) + months_ago = datetime.datetime.now() - datetime.timedelta( + days=int(months[0]) * 30 + ) + release_time = months_ago.strftime("%Y-%m-%d %H:%M:%S") + + elif "周前" in release_time: + weeks = re.compile("(\d+)周前").findall(release_time) + weeks_ago = datetime.datetime.now() - datetime.timedelta(days=int(weeks[0]) * 7) + release_time = weeks_ago.strftime("%Y-%m-%d %H:%M:%S") + + elif "天前" in release_time: + ndays = re.compile("(\d+)天前").findall(release_time) + days_ago = datetime.datetime.now() - datetime.timedelta(days=int(ndays[0])) + release_time = days_ago.strftime("%Y-%m-%d %H:%M:%S") + + elif "小时前" in release_time: + nhours = re.compile("(\d+)小时前").findall(release_time) + hours_ago = datetime.datetime.now() - datetime.timedelta(hours=int(nhours[0])) + release_time = hours_ago.strftime("%Y-%m-%d %H:%M:%S") + + elif "分钟前" in release_time: + nminutes = re.compile("(\d+)分钟前").findall(release_time) + minutes_ago = datetime.datetime.now() - datetime.timedelta( + minutes=int(nminutes[0]) + ) + release_time = minutes_ago.strftime("%Y-%m-%d %H:%M:%S") + + elif "昨天" in release_time or "昨日" in release_time: + today = datetime.date.today() + yesterday = today - datetime.timedelta(days=1) + release_time = release_time.replace("昨天", str(yesterday)) + + elif "今天" in release_time: + release_time = release_time.replace("今天", get_current_date("%Y-%m-%d")) + + elif "刚刚" in release_time: + release_time = get_current_date() + + elif re.search("^\d\d:\d\d", release_time): + release_time = get_current_date("%Y-%m-%d") + " " + release_time + + elif not re.compile("\d{4}").findall(release_time): + month = re.compile("\d{1,2}").findall(release_time) + if month and int(month[0]) <= int(get_current_date("%m")): + release_time = get_current_date("%Y") + "-" + release_time + else: + release_time = str(int(get_current_date("%Y")) - 1) + "-" + release_time + + release_time = format_date(release_time, new_format=date_format) + + return release_time + + +def to_date(date_str, date_format="%Y-%m-%d %H:%M:%S"): + return datetime.datetime.strptime(date_str, date_format) + + +def get_before_date( + current_date, + days, + current_date_format="%Y-%m-%d %H:%M:%S", + return_date_format="%Y-%m-%d %H:%M:%S", +): + """ + @summary: 获取之前时间 + --------- + @param current_date: 当前时间 str类型 + @param days: 时间间隔 -1 表示前一天 1 表示后一天 + @param days: 返回的时间格式 + --------- + @result: 字符串 + """ + + current_date = to_date(current_date, current_date_format) + date_obj = current_date + datetime.timedelta(days=days) + return datetime.datetime.strftime(date_obj, return_date_format) + + +def delay_time(sleep_time=160): + """ + @summary: 睡眠 默认1分钟 + --------- + @param sleep_time: 以秒为单位 + --------- + @result: + """ + + time.sleep(sleep_time) + + +def format_seconds(seconds): + """ + @summary: 将秒转为时分秒 + --------- + @param seconds: + --------- + @result: 2天3小时2分49秒 + """ + + seconds = int(seconds + 0.5) # 向上取整 + + m, s = divmod(seconds, 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + + times = "" + if d: + times += "{}天".format(d) + if h: + times += "{}小时".format(h) + if m: + times += "{}分".format(m) + if s: + times += "{}秒".format(s) + + return times + + +################################################ +def get_md5(*args): + """ + @summary: 获取唯一的32位md5 + --------- + @param *args: 参与联合去重的值 + --------- + @result: 7c8684bcbdfcea6697650aa53d7b1405 + """ + + m = hashlib.md5() + for arg in args: + m.update(str(arg).encode()) + + return m.hexdigest() + + +def get_sha1(*args): + """ + @summary: 获取唯一的40位值, 用于获取唯一的id + --------- + @param *args: 参与联合去重的值 + --------- + @result: ba4868b3f277c8e387b55d9e3d0be7c045cdd89e + """ + + sha1 = hashlib.sha1() + for arg in args: + sha1.update(str(arg).encode()) + return sha1.hexdigest() # 40位 + + +def get_base64(secret, message): + """ + @summary: 数字证书签名算法是:"HMAC-SHA256" + 参考:https://www.jokecamp.com/blog/examples-of-creating-base64-hashes-using-hmac-sha256-in-different-languages/ + --------- + @param secret: 秘钥 + @param message: 消息 + --------- + @result: 签名输出类型是:"base64" + """ + + import hashlib + import hmac + import base64 + + message = bytes(message, "utf-8") + secret = bytes(secret, "utf-8") + + signature = base64.b64encode( + hmac.new(secret, message, digestmod=hashlib.sha256).digest() + ).decode("utf8") + return signature + + +def get_uuid(key1="", key2=""): + """ + @summary: 计算uuid值 + 可用于将两个字符串组成唯一的值。如可将域名和新闻标题组成uuid,形成联合索引 + --------- + @param key1:str + @param key2:str + --------- + @result: + """ + + uuid_object = "" + + if not key1 and not key2: + uuid_object = uuid.uuid1() + else: + hash = md5(bytes(key1, "utf-8") + bytes(key2, "utf-8")).digest() + uuid_object = uuid.UUID(bytes=hash[:16], version=3) + + return str(uuid_object) + + +def get_hash(text): + return hash(text) + + +################################################## + + +def cut_string(text, length): + """ + @summary: 将文本按指定长度拆分 + --------- + @param text: 文本 + @param length: 拆分长度 + --------- + @result: 返回按指定长度拆分后形成的list + """ + + text_list = re.findall(".{%d}" % length, text, re.S) + leave_text = text[len(text_list) * length:] + if leave_text: + text_list.append(leave_text) + + return text_list + + +def get_random_string(length=1): + random_string = "".join(random.sample(string.ascii_letters + string.digits, length)) + return random_string + + +def get_random_password(length=8, special_characters=""): + """ + @summary: 创建随机密码 默认长度为8,包含大写字母、小写字母、数字 + --------- + @param length: 密码长度 默认8 + @param special_characters: 特殊字符 + --------- + @result: 指定长度的密码 + """ + + while True: + random_password = "".join( + random.sample( + string.ascii_letters + string.digits + special_characters, length + ) + ) + if ( + re.search("[0-9]", random_password) + and re.search("[A-Z]", random_password) + and re.search("[a-z]", random_password) + ): + if not special_characters: + break + elif set(random_password).intersection(special_characters): + break + + return random_password + + +def get_random_email(length=None, email_types: list = None, special_characters=""): + """ + 随机生成邮箱 + :param length: 邮箱长度 + :param email_types: 邮箱类型 + :param special_characters: 特殊字符 + :return: + """ + if not length: + length = random.randint(4, 12) + if not email_types: + email_types = [ + "qq.com", + "163.com", + "gmail.com", + "yahoo.com", + "hotmail.com", + "yeah.net", + "126.com", + "139.com", + "sohu.com", + ] + + email_body = get_random_password(length, special_characters) + email_type = random.choice(email_types) + + email = email_body + "@" + email_type + return email + + +################################# + + +def dumps_obj(obj): + return pickle.dumps(obj) + + +def loads_obj(obj_str): + return pickle.loads(obj_str) + + +def get_method(obj, name): + name = str(name) + try: + return getattr(obj, name) + except AttributeError: + log.error("Method %r not found in: %s" % (name, obj)) + return None + + +def witch_workspace(project_path): + """ + @summary: + --------- + @param project_path: + --------- + @result: + """ + + os.chdir(project_path) # 切换工作路经 + + +############### 数据库相关 ####################### +def format_sql_value(value): + if isinstance(value, str): + value = value.strip() + + elif isinstance(value, (list, dict)): + value = dumps_json(value, indent=None) + + elif isinstance(value, (datetime.date, datetime.time)): + value = str(value) + + elif isinstance(value, bool): + value = int(value) + + return value + + +def list2str(datas): + """ + 列表转字符串 + :param datas: [1, 2] + :return: (1, 2) + """ + data_str = str(tuple(datas)) + data_str = re.sub(",\)$", ")", data_str) + return data_str + + +def make_insert_sql( + table, data, auto_update=False, update_columns=(), insert_ignore=False +): + """ + @summary: 适用于mysql, oracle数据库时间需要to_date 处理(TODO) + --------- + @param table: + @param data: 表数据 json格式 + @param auto_update: 使用的是replace into, 为完全覆盖已存在的数据 + @param update_columns: 需要更新的列 默认全部,当指定值时,auto_update设置无效,当duplicate key冲突时更新指定的列 + @param insert_ignore: 数据存在忽略 + --------- + @result: + """ + + keys = ["`{}`".format(key) for key in data.keys()] + keys = list2str(keys).replace("'", "") + + values = [format_sql_value(value) for value in data.values()] + values = list2str(values) + + if update_columns: + if not isinstance(update_columns, (tuple, list)): + update_columns = [update_columns] + update_columns_ = ", ".join( + ["{key}=values({key})".format(key=key) for key in update_columns] + ) + sql = ( + "insert%s into {table} {keys} values {values} on duplicate key update %s" + % (" ignore" if insert_ignore else "", update_columns_) + ) + + elif auto_update: + sql = "replace into {table} {keys} values {values}" + else: + sql = "insert%s into {table} {keys} values {values}" % ( + " ignore" if insert_ignore else "" + ) + + sql = sql.format(table=table, keys=keys, values=values).replace("None", "null") + return sql + + +def make_update_sql(table, data, condition): + """ + @summary: 适用于mysql, oracle数据库时间需要to_date 处理(TODO) + --------- + @param table: + @param data: 表数据 json格式 + @param condition: where 条件 + --------- + @result: + """ + key_values = [] + + for key, value in data.items(): + value = format_sql_value(value) + if isinstance(value, str): + key_values.append("`{}`='{}'".format(key, value)) + elif value is None: + key_values.append("`{}`={}".format(key, "null")) + else: + key_values.append("`{}`={}".format(key, value)) + + key_values = ", ".join(key_values) + + sql = "update {table} set {key_values} where {condition}" + sql = sql.format(table=table, key_values=key_values, condition=condition) + return sql + + +def make_batch_sql( + table, datas, auto_update=False, update_columns=(), update_columns_value=() +): + """ + @summary: 生产批量的sql + --------- + @param table: + @param datas: 表数据 [{...}] + @param auto_update: 使用的是replace into, 为完全覆盖已存在的数据 + @param update_columns: 需要更新的列 默认全部,当指定值时,auto_update设置无效,当duplicate key冲突时更新指定的列 + @param update_columns_value: 需要更新的列的值 默认为datas里边对应的值, 注意 如果值为字符串类型 需要主动加单引号, 如 update_columns_value=("'test'",) + --------- + @result: + """ + if not datas: + return + + keys = list(datas[0].keys()) + values_placeholder = ["%s"] * len(keys) + + values = [] + for data in datas: + value = [] + for key in keys: + current_data = data.get(key) + current_data = format_sql_value(current_data) + + value.append(current_data) + + values.append(value) + + keys = ["`{}`".format(key) for key in keys] + keys = list2str(keys).replace("'", "") + + values_placeholder = list2str(values_placeholder).replace("'", "") + + if update_columns: + if not isinstance(update_columns, (tuple, list)): + update_columns = [update_columns] + if update_columns_value: + update_columns_ = ", ".join( + [ + "`{key}`={value}".format(key=key, value=value) + for key, value in zip(update_columns, update_columns_value) + ] + ) + else: + update_columns_ = ", ".join( + ["`{key}`=values(`{key}`)".format(key=key) for key in update_columns] + ) + sql = "insert into {table} {keys} values {values_placeholder} on duplicate key update {update_columns}".format( + table=table, + keys=keys, + values_placeholder=values_placeholder, + update_columns=update_columns_, + ) + elif auto_update: + sql = "replace into {table} {keys} values {values_placeholder}".format( + table=table, keys=keys, values_placeholder=values_placeholder + ) + else: + sql = "insert ignore into {table} {keys} values {values_placeholder}".format( + table=table, keys=keys, values_placeholder=values_placeholder + ) + + return sql, values + + +############### json相关 ####################### + + +def key2underline(key): + regex = "[A-Z]*" + capitals = re.findall(regex, key) + + if capitals: + for pos, capital in enumerate(capitals): + if not capital: + continue + if pos == 0: + if len(capital) > 1: + key = key.replace(capital, capital.lower() + "_", 1) + else: + key = key.replace(capital, capital.lower(), 1) + else: + if len(capital) > 1: + key = key.replace(capital, "_" + capital.lower() + "_", 1) + else: + key = key.replace(capital, "_" + capital.lower(), 1) + + return key.strip("_") + + +def key2hump(key): + """ + 下划线试变成首字母大写 + """ + return key.title().replace("_", "") + + +def format_json_key(json_data): + json_data_correct = {} + for key, value in json_data.items(): + key = key2underline(key) + json_data_correct[key] = value + + return json_data_correct + + +def quick_to_json(text): + """ + @summary: 可快速将浏览器上的header转为json格式 + --------- + @param text: + --------- + @result: + """ + + contents = text.split("\n") + json = {} + for content in contents: + if content == "\n": + continue + + content = content.strip() + regex = ["(:?.*?):(.*)", "(.*?):? +(.*)", "([^:]*)"] + + result = get_info(content, regex) + result = result[0] if isinstance(result[0], tuple) else result + try: + json[result[0]] = eval(result[1].strip()) + except: + json[result[0]] = result[1].strip() + + return json + + +############################## + + +def print_pretty(object): + pprint(object) + + +def print_params2json(url): + params_json = {} + params = url.split("?")[-1].split("&") + for param in params: + key_value = param.split("=", 1) + params_json[key_value[0]] = key_value[1] + + print(dumps_json(params_json)) + + +def print_cookie2json(cookie_str_or_list): + if isinstance(cookie_str_or_list, str): + cookie_json = {} + cookies = cookie_str_or_list.split("; ") + for cookie in cookies: + name, value = cookie.split("=") + cookie_json[name] = value + else: + cookie_json = get_cookies_from_selenium_cookie(cookie_str_or_list) + + print(dumps_json(cookie_json)) + + +############################### + + +def flatten(x): + """flatten(sequence) -> list + Returns a single, flat list which contains all elements retrieved + from the sequence and all recursively contained sub-sequences + (iterables). + Examples: + >>> [1, 2, [3,4], (5,6)] + [1, 2, [3, 4], (5, 6)] + >>> flatten([[[1,2,3], (42,None)], [4,5], [6], 7, (8,9,10)]) + [1, 2, 3, 42, None, 4, 5, 6, 7, 8, 9, 10] + >>> flatten(["foo", "bar"]) + ['foo', 'bar'] + >>> flatten(["foo", ["baz", 42], "bar"]) + ['foo', 'baz', 42, 'bar'] + """ + return list(iflatten(x)) + + +def iflatten(x): + """iflatten(sequence) -> iterator + Similar to ``.flatten()``, but returns iterator instead""" + for el in x: + if _is_listlike(el): + for el_ in flatten(el): + yield el_ + else: + yield el + + +def _is_listlike(x): + """ + >>> _is_listlike("foo") + False + >>> _is_listlike(5) + False + >>> _is_listlike(b"foo") + False + >>> _is_listlike([b"foo"]) + True + >>> _is_listlike((b"foo",)) + True + >>> _is_listlike({}) + True + >>> _is_listlike(set()) + True + >>> _is_listlike((x for x in range(3))) + True + >>> _is_listlike(six.moves.xrange(5)) + True + """ + return hasattr(x, "__iter__") and not isinstance(x, (six.text_type, bytes)) + + +################### + + +def re_def_supper_class(obj, supper_class): + """ + 重新定义父类 + @param obj: 类 如 class A: 则obj为A 或者 A的实例 a.__class__ + @param supper_class: 父类 + @return: + """ + obj.__bases__ = (supper_class,) + +################### + + +# def is_in_rate_limit(rate_limit, *key): +# """ +# 频率限制 +# :param rate_limit: 限制时间 单位秒 +# :param key: 限制频率的key +# :return: True / False +# """ +# msg_md5 = get_md5(*key) +# key = "rate_limit:{}".format(msg_md5) +# if get_redisdb().get(key): +# return True +# +# get_redisdb().set(key, time.time(), ex=rate_limit) +# return False + + +# def dingding_warning( +# message, +# rate_limit=3600, +# message_prefix=None, +# url=setting.DINGDING_WARNING_URL, +# user_phone=setting.DINGDING_WARNING_PHONE, +# ): +# if not url: +# log.info("未设置叮叮的地址,不支持报警") +# return +# +# if is_in_rate_limit(rate_limit, url, user_phone, message_prefix or message): +# log.info("报警时间间隔过短,此次报警忽略。 内容 {}".format(message)) +# return +# +# data = { +# "msgtype": "text", +# "text": {"content": message}, +# "at": {"atMobiles": [user_phone], "isAtAll": False}, +# } +# +# headers = {"Content-Type": "application/json"} +# +# response = requests.post(url, headers=headers, data=json.dumps(data).encode("utf8")) +# return response diff --git a/spiders/db/__init__.py b/spiders/db/__init__.py new file mode 100644 index 0000000..6ca20ac --- /dev/null +++ b/spiders/db/__init__.py @@ -0,0 +1,8 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: __init__.py +# Author: liangbaikai +# Date: 2021/1/8 +# Desc: there is a python file description +# ------------------------------------------------------------------ + diff --git a/spiders/db/sanicdb.py b/spiders/db/sanicdb.py new file mode 100644 index 0000000..f667663 --- /dev/null +++ b/spiders/db/sanicdb.py @@ -0,0 +1,176 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: sanicdb +# Author: liangbaikai +# Date: 2021/1/8 +# Desc: there is a python file description +# ------------------------------------------------------------------ + +# !/usr/bin/env python +"""A lightweight wrapper around aiomysql.Pool for easy to use +""" + +import traceback +import aiomysql +import pymysql + +version = "0.3" +version_info = (0, 3, 0, 0) + + +class SanicDB: + """A lightweight wrapper around aiomysql.Pool for easy to use + """ + + def __init__(self, host, database, user, password, port=3306, + loop=None, sanic=None, + minsize=3, maxsize=5, + return_dict=True, + pool_recycle=7 * 3600, + autocommit=True, + charset="utf8mb4", **kwargs): + ''' + kwargs: all parameters that aiomysql.connect() accept. + ''' + self.db_args = { + "port": port, + 'host': host, + 'db': database, + 'user': user, + 'password': password, + 'minsize': minsize, + 'maxsize': maxsize, + 'charset': charset, + 'loop': loop, + 'autocommit': autocommit, + 'pool_recycle': pool_recycle, + } + self.sanic = sanic + if sanic: + sanic.db = self + if return_dict: + self.db_args['cursorclass'] = aiomysql.cursors.DictCursor + if kwargs: + self.db_args.update(kwargs) + self.pool = None + + def close(self): + if self.pool: + self.pool.close() + + async def init_pool(self): + if self.sanic: + self.db_args['loop'] = self.sanic.loop + self.pool = await aiomysql.create_pool(**self.db_args) + + async def query_many(self, queries): + """query man SQL, Returns all result.""" + if not self.pool: + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + results = [] + for query in queries: + try: + await cur.execute(query) + ret = await cur.fetchall() + except pymysql.err.InternalError: + await conn.ping() + await cur.execute(query) + ret = await cur.fetchall() + results.append(ret) + return results + + async def query(self, query, *parameters, **kwparameters): + """Returns a row list for the given query and parameters.""" + if not self.pool: + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + try: + await cur.execute(query, kwparameters or parameters) + ret = await cur.fetchall() + except pymysql.err.InternalError: + await conn.ping() + await cur.execute(query, kwparameters or parameters) + ret = await cur.fetchall() + return ret + + + async def get(self, query, *parameters, **kwparameters): + """Returns the (singular) row returned by the given query. + """ + if not self.pool: + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + try: + await cur.execute(query, kwparameters or parameters) + ret = await cur.fetchone() + except pymysql.err.InternalError: + await conn.ping() + await cur.execute(query, kwparameters or parameters) + ret = await cur.fetchone() + return ret + + async def execute(self, query, *parameters, **kwparameters): + """Executes the given query, returning the lastrowid from the query.""" + if not self.pool: + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor() as cur: + try: + await cur.execute(query, kwparameters or parameters) + except Exception: + # https://github.com/aio-libs/aiomysql/issues/340 + await conn.ping() + await cur.execute(query, kwparameters or parameters) + return cur.lastrowid + + # high level interface + async def table_has(self, table_name, field, value): + sql = 'SELECT {} FROM {} WHERE {}=%s limit 1'.format(field, table_name, field) + d = await self.get(sql, value) + return d + + async def table_insert(self, table_name, item, ignore_duplicated=True): + '''item is a dict : key is mysql table field''' + fields = list(item.keys()) + values = list(item.values()) + fieldstr = ','.join(fields) + valstr = ','.join(['%s'] * len(item)) + sql = 'INSERT INTO %s (%s) VALUES(%s)' % (table_name, fieldstr, valstr) + try: + last_id = await self.execute(sql, *values) + return last_id + except Exception as e: + if ignore_duplicated and e.args[0] == 1062: + # just skip duplicated item + return 0 + traceback.print_exc() + print('sql:', sql) + print('item:') + for i in range(len(fields)): + vs = str(values[i]) + if len(vs) > 300: + print(fields[i], ' : ', len(vs), type(values[i])) + else: + print(fields[i], ' : ', vs, type(values[i])) + raise e + + async def table_update(self, table_name, updates, + field_where, value_where): + '''updates is a dict of {field_update:value_update}''' + upsets = [] + values = [] + for k, v in updates.items(): + s = '%s=%%s' % k + upsets.append(s) + values.append(v) + upsets = ','.join(upsets) + sql = 'UPDATE %s SET %s WHERE %s="%s"' % ( + table_name, + upsets, + field_where, value_where, + ) + await self.execute(sql, *(values)) diff --git a/spiders/govs.py b/spiders/govs.py new file mode 100644 index 0000000..9fb6de6 --- /dev/null +++ b/spiders/govs.py @@ -0,0 +1,47 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: govs +# Author: liangbaikai +# Date: 2021/1/6 +# Desc: there is a python file description +# ------------------------------------------------------------------ +import traceback + +from smart.field import HtmlField +from smart.item import Item +from smart.request import Request +from smart.response import Response +from smart.spider import Spider + + +class ArticelItem(Item): + title = HtmlField(xpath_select="//*[@class='titles']/text()") + pub_time = HtmlField(xpath_select="//*[@class='times']/text()") + author = HtmlField(xpath_select="//*[@class='author']/text()") + content = HtmlField(xpath_select="//div[@class='article-content']") + + +class GovsSpider(Spider): + name = "GovsSpider" + start_urls = [ + "http://www.nea.gov.cn/policy/jd.htm" + ] + + def parse(self, response: Response): + selects_detail_urls = response.xpath( + '//*[@class="list"]//li//a/@href').getall() + if len(selects_detail_urls) > 0: + for detail_url in selects_detail_urls: + yield Request(url=detail_url, + callback=self.parse_detail) + next = response.xpath('//*[@id="div_currpage"]//a[text()="下一页"]') + if next: + next_url = response.xpath( + '//*[@id="div_currpage"]//a[text()="下一页"]/@href').get() + yield Request(url=next_url, callback=self.parse) + + def parse_detail(self, response): + yield ArticelItem.get_item(html=response.text) + + def on_exception_occured(self, e: Exception): + print(e) diff --git a/spiders/ipspider.py b/spiders/ipspider.py new file mode 100644 index 0000000..6176f77 --- /dev/null +++ b/spiders/ipspider.py @@ -0,0 +1,21 @@ + +from tinepeas import Spider, Request + + +class IpSpider(Spider): + name = 'ipspider' + start_urls = [ + "http://www.nea.gov.cn/xwzx/nyyw.htm" + ] + + def parse(self, response): + print("run parse...........................................") + print(response.status) + res = response.xpath("/html/body//div/ul[@class='list']/li/a/@href") + getall = res.getall() + for _url in getall: + yield Request(url=str(_url), callback=self.parse_detail, dont_filter=True) + print(22) + + def parse_detail(self, response): + print(333) diff --git a/spiders/ipspider2.py b/spiders/ipspider2.py new file mode 100644 index 0000000..0aaddee --- /dev/null +++ b/spiders/ipspider2.py @@ -0,0 +1,131 @@ +import json +import threading + +from smart.item import Item +from smart.response import Response +from smart.request import Request +from smart.spider import Spider + + +class TestItem(Item): + name = "23232" + age = 900 + + +class IpSpider(Spider): + name = 'ipspider2' + start_urls = [] + cutome_setting_dict = {**Spider.cutome_setting_dict, **{"req_per_concurrent": 100}} + + def start_requests(self): + for page in range(100): + url = f'http://exercise.kingname.info/exercise_middleware_ip/{page}' + # url = f'http://exercise.kingname.info/exercise_middleware_ip/{page}' + # url = 'http://fzggw.zj.gov.cn/art/2020/8/26/art_1621004_55344873.html' + url = 'https://s.bdstatic.com/common/openjs/amd/eslx.js' + yield Request(url, callback=self.parse, dont_filter=True, timeout=3) + + def parse(self, response: Response): + pass + # yield TestItem(response.text) + # for page in range(10): + # print(page) + # url = f'http://exercise.kingname.info/exercise_middleware_ip/{page}' + # # url = f'http://exercise.kingname.info/exercise_middleware_ip/{page}' + # # url = 'http://fzggw.zj.gov.cn/art/2020/8/26/art_1621004_55344873.html' + # url = 'https://s.bdstatic.com/common/openjs/amd/eslx.js' + # yield Request(url, callback=self.parse2, dont_filter=True, timeout=3) + # print(response.status) + # yield Request(response.url, callback=self.parse2, dont_filter=True) + + def parse2(self, response): + print(response.status) + print("parse2222") + + def on_close(self): + print('我被关闭了') + + +class IpSpider3(Spider): + name = 'IpSpider22222' + start_urls = [] + + def on_start(self): + self.cutome_setting_dict.update({ + + }) + print('IpSpider22222 started') + + def start_requests(self): + for page in range(1122): + # url = f'http://exercise.kingname.info/exercise_middleware_ip/{page}' + url = 'https://s.bdstatic.com/common/openjs/amd/eslx.js' + yield Request(url, callback=self.parse, dont_filter=True) + + def parse(self, response: Response): + # print('#######2') + print(f'#######{self.name}') + # print(threading.current_thread().name, "runing...", self.name) + print(response.status) + print("222222222222222") + yield Request(url=response.url, callback=self.parse2, dont_filter=True) + + def parse2(self, response): + print("222222222222222") + pass + + def on_close(self): + print('我被关闭了') + + +class GovSpider(Spider): + name = 'GovSpider' + start_urls = [ + "http://www.nea.gov.cn/xwzx/nyyw.htm" + ] + + def on_start(self): + print('GovSpider started') + + def parse(self, response: Response): + print("run parse...........................................") + print(response.status) + res = response.xpath("/html/body//div/ul[@class='list']/li/a/@href") + getall = res.getall() + + for _url in getall: + yield Request(url=_url, callback=self.parse_detail, dont_filter=True) + print(22222222222222222222222222222222222222) + print(3333) + + def parse_detail(self, response: Response): + print("xxxxxxxxxxparse_detail") + + def on_close(self): + print(' GovSpider 关闭了') + + def on_exception_occured(self, e: Exception): + print(e) + + +class ApiSpider(Spider): + name = 'ApiSpider' + start_urls = [ + "http://search.51job.com" + ] + + def on_start(self): + print('ApiSpider started') + + def start_requests(self): + for i in range(1): + yield Request(url="http://search.51job.com", callback=self.parse, dont_filter=True, + ) + + def parse(self, response: Response): + print("run parse...........................................") + print(response.status) + print(response.text) + + def on_exception_occured(self, e: Exception): + print(e) diff --git a/spiders/js/__init__.py b/spiders/js/__init__.py new file mode 100644 index 0000000..a06c58c --- /dev/null +++ b/spiders/js/__init__.py @@ -0,0 +1,8 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: __init__.py +# Author: liangbaikai +# Date: 2021/1/7 +# Desc: there is a python file description +# ------------------------------------------------------------------ + diff --git a/spiders/js/js_spider.py b/spiders/js/js_spider.py new file mode 100644 index 0000000..4256bc3 --- /dev/null +++ b/spiders/js/js_spider.py @@ -0,0 +1,56 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: js_spider +# Author: liangbaikai +# Date: 2021/1/7 +# Desc: there is a python file description +# ------------------------------------------------------------------ +from asyncio import Lock + +from pyppeteer import launch + +from smart.downloader import BaseDown +from smart.request import Request +from smart.response import Response +from smart.spider import Spider + + +class Broswer(BaseDown): + + def __init__(self): + self.browser = None + self.lock = Lock() + + async def fetch(self, request: Request) -> Response: + # 双重检查锁 “并发” 防止打开多个浏览器 + if self.browser is None: + async with self.lock: + if self.browser is None: + self.browser = await launch({ + 'headless': False, + 'dumpio': True, # 'dumpio':True 浏览器就不会卡住了 + 'autoClose': True, + 'executablePath': r'D:\soft\googlechrome\Application\77.0.3865.120\chrome.exe', + # 浏览器的存放地址,指定路径可快速运行 + 'args': ['–no - sandbox'] + }) + + page = await self.browser.newPage() + res = await page.goto(request.url) + page_text = await page.content() + await page.close() + return Response(body=page_text.encode(), status=res.status, request=request, headers=res.headers) + + +class JsSpider(Spider): + + cutome_setting_dict = {**Spider.cutome_setting_dict, + **{"net_download_class": "spiders.js.js_spider.Broswer", "req_per_concurrent": 15}} + + def start_requests(self): + start_urls = ["https://www.jianshu.com/p/e8f7f6c82be6" for i in range(30)] + for url in start_urls: + yield Request(url=url, callback=self.parse, dont_filter=True) + + def parse(self, response: Response): + print(response.ok) diff --git a/spiders/json_spider.py b/spiders/json_spider.py new file mode 100644 index 0000000..caf43d1 --- /dev/null +++ b/spiders/json_spider.py @@ -0,0 +1,36 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: JsonSpider +# Author: liangbaikai +# Date: 2021/1/7 +# Desc: there is a python file description +# ------------------------------------------------------------------ +import re +import traceback + +from smart.field import JsonPathField, RegexField +from smart.item import Item +from smart.response import Response +from smart.spider import Spider + + +class BidItem(Item): + target_item = JsonPathField(json_path="$.data.records.*") + + cate = JsonPathField(json_path="$..cate") + city = JsonPathField(json_path="$..city") + pub_time = JsonPathField(json_path="$..pub_time") + title = RegexField(re_select='"title":.?"(.*?)",') + + def clean_city(self, value): + return value + "省" + + +class JsonSpider(Spider): + name = "JsonSpider" + start_urls = [ + "http://139.155.14.219:8529/suining/bid/pageTop?current=1&size=50&sort=id,desc" + ] + + def parse(self, response: Response): + yield from BidItem.get_items(response.text) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..225220e --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,38 @@ +import asyncio +import time +from concurrent.futures.thread import ThreadPoolExecutor + +from smart.middlewire import Middleware + +middleware2 = Middleware() + +total_res = 0 +succedd = 0 + + +class ReqInte: + + @staticmethod + @middleware2.request(-1) + def print_on_request1(spider_ins, request): + print(f"ReqInteReqInteReqInteReqInt{spider_ins.name} e#{request}######################") + + +@middleware2.request(1) +async def print_on_request(spider_ins, request): + request.metadata = {"url": request.url} + global total_res + total_res += 1 + print(f"requesssst: {request.metadata}") + print(f"total_res: {total_res}") + + # Just operate request object, and do not return anything. + + +@middleware2.response +def print_on_response(spider_ins, request, response): + if response and 0 < response.status <= 200: + global succedd + succedd += 1 + print(f"response0: {response.status}") + print(f"succedd: {succedd}") diff --git a/test/db_test.py b/test/db_test.py new file mode 100644 index 0000000..90234bd --- /dev/null +++ b/test/db_test.py @@ -0,0 +1,73 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: db_test +# Author: liangbaikai +# Date: 2021/1/8 +# Desc: there is a python file description +# ------------------------------------------------------------------ + +import asyncio + +from spiders.db.sanicdb import SanicDB + + +async def test(loop): + db = SanicDB('localhost', 'testdb', 'root', 'root', + minsize=3, maxsize=5, + connect_timeout=5, + loop=None) + sql = 'Drop table test' + await db.execute(sql) + + sql = """CREATE TABLE `test` ( + `id` int(8) NOT NULL AUTO_INCREMENT, + `name` varchar(16) NOT NULL, + `content` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) + ) ENGINE=MyISAM ;""" + await db.execute(sql) + + sql = 'select * from test where name = %s' + data = await db.query(sql, 'abc') + print('query():', data) + + sql += ' limit 1' + d = await db.get(sql, 'abc') + print('get():', d) + + sql = 'delete from test where name=%s' + lastrowid = await db.execute(sql, 'xyz') + print('execute(delete...):', lastrowid) + sql = 'insert into test set name=%s, content=%s' + lastrowid = await db.execute(sql, 'xyz', '456') + print('execute(insert...):', lastrowid) + + ret = await db.table_has('test', 'name', 'abc') + print('has(): ', ret) + + ret = await db.table_update('test', {'content': 'updated'}, + 'name', 'abc') + print('update():', ret) + sql = 'select * from test where name = %s' + data = await db.query(sql, 'abc') + print('query():', data) + + item = { + 'name': 'abc', + 'content': '123' + } + i = 0 + while 1: + i += 1 + if i % 2 == 0: + lastid = await db.table_insert('test', item, ignore_duplicated=True) + else: + lastid = await db.table_insert('test', item) + item.update(name=i) + print('insert():', lastid) + + +if __name__ == '__main__': + loop = asyncio.get_event_loop() + loop.run_until_complete(test(loop)) diff --git a/test/ruia_test.py b/test/ruia_test.py new file mode 100644 index 0000000..780bd05 --- /dev/null +++ b/test/ruia_test.py @@ -0,0 +1,73 @@ +# -*- coding utf-8 -*-# +# ------------------------------------------------------------------ +# Name: ruia_test +# Author: liangbaikai +# Date: 2020/12/31 +# Desc: there is a python file description +# ------------------------------------------------------------------ + +from ruia import AttrField, Item, Request, Spider, TextField +from ruia_ua import middleware + + +class ArchivesItem(Item): + """ + eg: http://www.ruanyifeng.com/blog/archives.html + """ + target_item = TextField(css_select='div#beta-inner li.module-list-item') + href = AttrField(css_select='li.module-list-item>a', attr='href') + + +class ArticleListItem(Item): + """ + eg: http://www.ruanyifeng.com/blog/essays/ + """ + target_item = TextField(css_select='div#alpha-inner li.module-list-item') + title = TextField(css_select='li.module-list-item>a') + href = AttrField(css_select='li.module-list-item>a', attr='href') + + +class BlogSpider(Spider): + """ + 针对博客源 http://www.ruanyifeng.com/blog/archives.html 的爬虫 + 这里为了模拟ua,引入了一个ruia的第三方扩展 + - ruia-ua: https://github.com/howie6879/ruia-ua + - pipenv install ruia-ua + - 此扩展会自动为每一次请求随机添加 User-Agent + """ + # 设置启动URL + start_urls = ['http://www.ruanyifeng.com/blog/archives.html'] + # 爬虫模拟请求的配置参数 + request_config = { + 'RETRIES': 3, + 'DELAY': 0, + 'TIMEOUT': 3 + } + # 请求信号量 + concurrency = 400 + blog_nums = 0 + + async def parse(self, res): + for page in range(1113): + print(page) + url = f'http://exercise.kingname.info/exercise_middleware_ip/{page}' + yield Request( + url, + callback=self.parse_item + ) + + async def parse_item(self, res): + print(res.html) + + + +class RuiaTestSpider(Spider): + request_config = { + 'RETRIES': 3, + 'DELAY': 0, + 'TIMEOUT': 3 + } + pass + +if __name__ == '__main__': + BlogSpider.start(middleware=middleware) diff --git a/test/test.html b/test/test.html new file mode 100644 index 0000000..2188a12 --- /dev/null +++ b/test/test.html @@ -0,0 +1,304 @@ + + + + + 政策法规 + + + + + + + + + +
+ +
+
+ + + + + +
+
+ + + +
+
+
当前位置:首页 > 能源经济 > 政策法规
+ +
+ +
北京9月30日电(记者毕磊)近日,国新办举行《关于全面提升“获得电力”服务水平 持续优化用电营商环境的意见》国务院政策例行吹风会。国家能源局副局长 + ... [更多详细]
+
发布日期:2020-09-30
+
+ +
+ +
北京9月30日电(记者毕磊)近日,国新办举行《关于全面提升“获得电力”服务水平 持续优化用电营商环境的意见》国务院政策例行吹风会。国家能源局市场监 + ... [更多详细]
+
发布日期:2020-09-30
+
+ +
+ +
国家煤矿安监局办公室关于印发《落实煤矿企业安全生产主体责任三年行动专题实施方案》的通知煤安监司办〔2020〕26号各产煤省、自治区、直辖市及新疆生产建设兵团煤矿安全监 ... + [更多详细]
+
发布日期:2020-09-18
+
+ +
+ +
8月27日,国家煤矿安全监察局副局长宋元明在2020年夏季度全国煤炭交易会上表示,国家煤监局正联合人社部、国家能源局、全国总工会,制定进一步规范煤矿劳动用工的指导意见 ... + [更多详细]
+
发布日期:2020-08-31
+
+ +
+ +
据国家能源局网站消息,近日,国家能源局电力业务资质管理中心编制并发布《能源行业信用状况年度报告(2020)》(以下简称《报告》)。《报告》显示,2019年我国能源行业市 ... + [更多详细]
+
发布日期:2020-08-31
+
+ +
+ +
国家能源局公告2020年 第3号  为贯彻落实《优化营商环境条例》,进一步减证便民、优化服务,国家能源局决定取消电力业务许可、承装(修、试)电力设施许可涉及的21项证明 ... + [更多详细]
+
发布日期:2020-08-29
+
+ +
+ +
近日,《国家能源局派出机构权力和责任清单》(2020年版)对外公布。该清单是对《国家能源局派出机构权力和责任清单(试行)》(2015年版)进行的修订。  2020年版权责清 + ... [更多详细]
+
发布日期:2020-08-29
+
+ +
+ +
国家发展改革委、国家能源局近日联合印发了《关于各省级行政区域2020年可再生能源电力消纳责任权重的通知》(发改能源〔2020〕767号)(以下简称《通知》),现从文件出台 + ... [更多详细]
+
发布日期:2020-08-29
+
+ +
+ +
国家可再生能源信息管理中心   2020年3月,国家能源局发布了《关于2020年风电、光伏发电项目建设有关事项的通知》(国能发新能〔2020〕17号,以下简称《通知》),启动了 + ... [更多详细]
+
发布日期:2020-08-29
+
+ +
+ +
当前,电力行业从高速增长进入高质量发展阶段,新建输变电工程投资需求放缓,技术改造、自动化、信息化、客户服务等领域投资占比逐步提升。随着“放管服”改革不 + ... [更多详细]
+
发布日期:2020-08-29
+
+ +
+ +
为落实《中共中央 国务院关于进一步深化电力体制改革的若干意见》(中发〔2015〕9号)及其配套文件精神,适应电力现货市场试点地区连续试结算工作需要,国家发展改革委、国 ... + [更多详细]
+
发布日期:2020-08-29
+
+ +
+ +
公开事项名称:国家能源局综合司关于加强电力行业危化品储存等安全防范工作的通知 国能综通安全〔2020〕85号索引号:000019705/2020-00058主办单位:国家能源局 + 制发日期:2020 ... [更多详细]
+
发布日期:2020-08-29
+
+ +
+
+
+
+ +
+ + +
+
+

热点会展

+

更多

+
+ +
+
+
+
+
1970-01-01 08:00:00
+
+
+ + + +
+ +
+ +
+ +
+ + + +
+ +
+ + + +